diff --git a/tests/Benchmarks/Benchmark.Api/Benchmark.Api.csproj b/tests/Benchmarks/Benchmark.Api/Benchmark.Api.csproj new file mode 100644 index 00000000..ecc56fce --- /dev/null +++ b/tests/Benchmarks/Benchmark.Api/Benchmark.Api.csproj @@ -0,0 +1,21 @@ + + + + netstandard2.1 + SpaceEngineers.Core.Benchmark.Api + SpaceEngineers.Core.Benchmark.Api + false + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + diff --git a/tests/Benchmarks/Benchmark.Api/Benchmark.cs b/tests/Benchmarks/Benchmark.Api/Benchmark.cs new file mode 100644 index 00000000..c8cf9ffa --- /dev/null +++ b/tests/Benchmarks/Benchmark.Api/Benchmark.cs @@ -0,0 +1,181 @@ +namespace SpaceEngineers.Core.Benchmark.Api +{ + using System; + using System.Collections.Generic; + using System.Diagnostics.CodeAnalysis; + using System.Globalization; + using System.Linq; + using BenchmarkDotNet.Columns; + using BenchmarkDotNet.Reports; + using BenchmarkDotNet.Running; + using BenchmarkDotNet.Validators; + using Perfolizer.Horology; + + /// + /// Benchmark entry point + /// + [SuppressMessage("Analysis", "CA1724", Justification = "desired name")] + public static class Benchmark + { + /// + /// Benchmark entry point method + /// + /// Output + /// TSource type-argument + /// Benchmark summary + public static Summary Run(Action output) + { + var summary = BenchmarkRunner.Run(); + + output(summary.Title); + output($"TotalTime: {summary.TotalTime.ToString()}"); + output($"Details in: {summary.LogFilePath}"); + + if (summary.HasCriticalValidationErrors) + { + var errors = ((IEnumerable)summary.ValidationErrors) + .Select(error => new InvalidOperationException(error.ToString())) + .ToList(); + + throw new AggregateException(errors); + } + + return summary; + } + + /// + /// Gets time measure in seconds from benchmark summary + /// + /// Summary + /// Measure record name + /// Measure + /// Output + /// Seconds time measure + public static decimal SecondMeasure( + this Summary summary, + string measureRecordName, + Measure measure, + Action output) + { + return summary.TimeMeasure( + TimeUnit.Second, + measureRecordName, + measure, + output); + } + + /// + /// Gets time measure in milliseconds from benchmark summary + /// + /// Summary + /// Measure record name + /// Measure + /// Output + /// Milliseconds time measure + public static decimal MillisecondMeasure( + this Summary summary, + string measureRecordName, + Measure measure, + Action output) + { + return summary.TimeMeasure( + TimeUnit.Millisecond, + measureRecordName, + measure, + output); + } + + /// + /// Gets time measure in nanoseconds from benchmark summary + /// + /// Summary + /// Measure record name + /// Measure + /// Output + /// Nanoseconds time measure + public static decimal NanosecondMeasure( + this Summary summary, + string measureRecordName, + Measure measure, + Action output) + { + return summary.TimeMeasure( + TimeUnit.Nanosecond, + measureRecordName, + measure, + output); + } + + /// + /// Gets time measure from benchmark summary + /// + /// Summary + /// TimeUnit + /// Measure record name + /// Measure + /// Output + /// Time measure + public static decimal TimeMeasure( + this Summary summary, + TimeUnit timeUnit, + string measureRecordName, + Measure measure, + Action output) + { + var timeMeasure = summary.TimeMeasures(measure, timeUnit)[measureRecordName]; + + output($"{measureRecordName} -> {measure} -> {timeMeasure} {timeUnit.Name}"); + + return timeMeasure; + } + + private static IDictionary TimeMeasures( + this Summary summary, + Measure measure, + TimeUnit timeUnit) + { + var measureColumnName = measure.ToString(); + + var methodColumn = summary.Column("Method"); + var measureColumn = summary.Column(measureColumnName); + + if (!measureColumn.IsNumeric) + { + throw new InvalidOperationException($"{measureColumnName} isn't numeric"); + } + + var style = new SummaryStyle(CultureInfo.InvariantCulture, + false, + SizeUnit.B, + timeUnit, + false, + true); + + return summary + .BenchmarksCases + .ToDictionary(benchmarksCase => methodColumn.GetValue(summary, benchmarksCase), + ParseTime(summary, methodColumn, measureColumn, measureColumnName, timeUnit, style)); + + static Func ParseTime( + Summary summary, + IColumn methodColumn, + IColumn measureColumn, + string measureColumnName, + TimeUnit timeUnit, + SummaryStyle style) + { + return benchmarkCase => + { + var measure = measureColumn.GetValue(summary, benchmarkCase, style); + + return decimal.Parse(measure, CultureInfo.InvariantCulture); + }; + } + } + + private static IColumn Column(this Summary summary, string columnName) + { + return summary.GetColumns().Single(col => col.ColumnName == columnName); + } + } +} \ No newline at end of file diff --git a/tests/Benchmarks/Benchmark.Api/Measure.cs b/tests/Benchmarks/Benchmark.Api/Measure.cs new file mode 100644 index 00000000..93ba933c --- /dev/null +++ b/tests/Benchmarks/Benchmark.Api/Measure.cs @@ -0,0 +1,13 @@ +namespace SpaceEngineers.Core.Benchmark.Api +{ + /// + /// Measure + /// + public enum Measure + { + /// + /// Mean + /// + Mean + } +} \ No newline at end of file diff --git a/tests/Benchmarks/Benchmark.Api/Properties/AssemblyInfo.cs b/tests/Benchmarks/Benchmark.Api/Properties/AssemblyInfo.cs new file mode 100644 index 00000000..31adb06d --- /dev/null +++ b/tests/Benchmarks/Benchmark.Api/Properties/AssemblyInfo.cs @@ -0,0 +1,5 @@ +using System.Reflection; + +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] +[assembly: AssemblyInformationalVersion("1.0.0.0")] \ No newline at end of file diff --git a/tests/Benchmarks/GenericHost.Benchmark/Benchmarks.cs b/tests/Benchmarks/GenericHost.Benchmark/Benchmarks.cs new file mode 100644 index 00000000..b7255b7b --- /dev/null +++ b/tests/Benchmarks/GenericHost.Benchmark/Benchmarks.cs @@ -0,0 +1,154 @@ +namespace SpaceEngineers.Core.GenericHost.Benchmark +{ + using System.Threading.Tasks; + using Core.Benchmark.Api; + using Sources; + using Test.Api; + using Test.Api.ClassFixtures; + using Xunit; + using Xunit.Abstractions; + + /// + /// Benchmarks + /// + // TODO: #136 - remove magic numbers and use adaptive approach -> store test artifacts in DB and search performance change points + public class Benchmarks : TestBase + { + /// .cctor + /// ITestOutputHelper + /// TestFixture + public Benchmarks(ITestOutputHelper output, TestFixture fixture) + : base(output, fixture) + { + } + + [Fact(Timeout = 300_000)] + internal void DatabaseConnectionProviderBenchmark() + { + var summary = Benchmark.Run(Output.WriteLine); + + var read = summary.MillisecondMeasure( + nameof(DatabaseConnectionProviderBenchmarkSource.Read), + Measure.Mean, + Output.WriteLine); + + var insert = summary.MillisecondMeasure( + nameof(DatabaseConnectionProviderBenchmarkSource.Insert), + Measure.Mean, + Output.WriteLine); + + var delete = summary.MillisecondMeasure( + nameof(DatabaseConnectionProviderBenchmarkSource.Delete), + Measure.Mean, + Output.WriteLine); + + Assert.True(read < 25m); + Assert.True(insert < 25m); + Assert.True(delete < 25m); + } + + [Fact(Timeout = 300_000)] + internal static async Task DatabaseConnectionProviderBenchmarkTest() + { + var source = new DatabaseConnectionProviderBenchmarkSource(); + + try + { + source.GlobalSetup(); + + for (var i = 0; i < 1000; i++) + { + source.IterationSetup(); + + await source.Read().ConfigureAwait(false); + await source.Insert().ConfigureAwait(false); + await source.Delete().ConfigureAwait(false); + + source.IterationCleanup(); + } + } + finally + { + source.GlobalCleanup(); + } + } + + [Fact(Timeout = 300_000)] + internal void MessageHandlerMiddlewareBenchmark() + { + var summary = Benchmark.Run(Output.WriteLine); + + var compositeMiddleware = summary.MillisecondMeasure( + nameof(MessageHandlerMiddlewareBenchmarkSource.RunCompositeMiddleware), + Measure.Mean, + Output.WriteLine); + + var tracingMiddleware = summary.MillisecondMeasure( + nameof(MessageHandlerMiddlewareBenchmarkSource.RunTracingMiddleware), + Measure.Mean, + Output.WriteLine); + + var errorHandlingMiddleware = summary.MillisecondMeasure( + nameof(MessageHandlerMiddlewareBenchmarkSource.RunErrorHandlingMiddleware), + Measure.Mean, + Output.WriteLine); + + var authorizationMiddleware = summary.MillisecondMeasure( + nameof(MessageHandlerMiddlewareBenchmarkSource.RunAuthorizationMiddleware), + Measure.Mean, + Output.WriteLine); + + var unitOfWorkMiddleware = summary.MillisecondMeasure( + nameof(MessageHandlerMiddlewareBenchmarkSource.RunUnitOfWorkMiddleware), + Measure.Mean, + Output.WriteLine); + + var handledByEndpointMiddleware = summary.MillisecondMeasure( + nameof(MessageHandlerMiddlewareBenchmarkSource.RunHandledByEndpointMiddleware), + Measure.Mean, + Output.WriteLine); + + var requestReplyMiddleware = summary.MillisecondMeasure( + nameof(MessageHandlerMiddlewareBenchmarkSource.RunRequestReplyMiddleware), + Measure.Mean, + Output.WriteLine); + + Assert.True(compositeMiddleware < 50m); + Assert.True(tracingMiddleware < 1m); + Assert.True(errorHandlingMiddleware < 1m); + Assert.True(authorizationMiddleware < 1m); + Assert.True(unitOfWorkMiddleware < 25m); + Assert.True(handledByEndpointMiddleware < 1m); + Assert.True(requestReplyMiddleware < 1m); + } + + [Fact(Timeout = 300_000)] + internal static async Task MessageHandlerMiddlewareBenchmarkTest() + { + var source = new MessageHandlerMiddlewareBenchmarkSource(); + + try + { + source.GlobalSetup(); + + for (var i = 0; i < 1000; i++) + { + source.IterationSetup(); + + await source.RunTracingMiddleware().ConfigureAwait(false); + await source.RunErrorHandlingMiddleware().ConfigureAwait(false); + await source.RunAuthorizationMiddleware().ConfigureAwait(false); + await source.RunUnitOfWorkMiddleware().ConfigureAwait(false); + await source.RunHandledByEndpointMiddleware().ConfigureAwait(false); + await source.RunRequestReplyMiddleware().ConfigureAwait(false); + + source.IterationCleanup(); + } + } + finally + { + source.GlobalCleanup(); + } + } + } +} \ No newline at end of file diff --git a/tests/Benchmarks/GenericHost.Benchmark/GenericHost.Benchmark.csproj b/tests/Benchmarks/GenericHost.Benchmark/GenericHost.Benchmark.csproj new file mode 100644 index 00000000..ccd12055 --- /dev/null +++ b/tests/Benchmarks/GenericHost.Benchmark/GenericHost.Benchmark.csproj @@ -0,0 +1,73 @@ + + + + net7.0 + GenericHost.Benchmark + SpaceEngineers.Core.GenericHost.Benchmark + false + false + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + + + + + + + + + + MessageHandlerMiddlewareBenchmarkSource.cs + + + MessageHandlerMiddlewareBenchmarkSource.cs + + + MessageHandlerMiddlewareBenchmarkSource.cs + + + DatabaseConnectionProviderBenchmarkSource.cs + + + DatabaseConnectionProviderBenchmarkSource.cs + + + DatabaseConnectionProviderBenchmarkSource.cs + + + DatabaseConnectionProviderBenchmarkSource.cs + + + DatabaseConnectionProviderBenchmarkSource.cs + + + DatabaseConnectionProviderBenchmarkSource.cs + + + DatabaseConnectionProviderBenchmarkSource.cs + + + DatabaseConnectionProviderBenchmarkSource.cs + + + diff --git a/tests/Benchmarks/GenericHost.Benchmark/Program.cs b/tests/Benchmarks/GenericHost.Benchmark/Program.cs new file mode 100644 index 00000000..a93a5b53 --- /dev/null +++ b/tests/Benchmarks/GenericHost.Benchmark/Program.cs @@ -0,0 +1,17 @@ +namespace SpaceEngineers.Core.GenericHost.Benchmark +{ + /// + /// Program + /// + public static class Program + { + /// Main + /// args + public static void Main(string[] args) + { + Benchmarks + .MessageHandlerMiddlewareBenchmarkTest() + .Wait(); + } + } +} \ No newline at end of file diff --git a/tests/Benchmarks/GenericHost.Benchmark/Properties/AssemblyInfo.cs b/tests/Benchmarks/GenericHost.Benchmark/Properties/AssemblyInfo.cs new file mode 100644 index 00000000..31adb06d --- /dev/null +++ b/tests/Benchmarks/GenericHost.Benchmark/Properties/AssemblyInfo.cs @@ -0,0 +1,5 @@ +using System.Reflection; + +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] +[assembly: AssemblyInformationalVersion("1.0.0.0")] \ No newline at end of file diff --git a/tests/Benchmarks/GenericHost.Benchmark/Settings/DatabaseConnectionProviderBenchmarkSource/appsettings.json b/tests/Benchmarks/GenericHost.Benchmark/Settings/DatabaseConnectionProviderBenchmarkSource/appsettings.json new file mode 100644 index 00000000..ec5785bb --- /dev/null +++ b/tests/Benchmarks/GenericHost.Benchmark/Settings/DatabaseConnectionProviderBenchmarkSource/appsettings.json @@ -0,0 +1,52 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Debug", + "System": "Information", + "Microsoft": "Information", + "Npgsql": "Information" + } + }, + "Transports": { + "RabbitMqIntegrationTransport": { + "RabbitMqSettings": { + "Hosts": [ + "localhost" + ], + "Port": "5672", + "HttpApiPort": "15672", + "User": "guest", + "Password": "guest", + "VirtualHost": "DatabaseConnectionProviderBenchmarkSource", + "ApplicationName": "DatabaseConnectionProviderBenchmarkSource", + "ConsumerPrefetchCount": 100, + "QueueMaxLengthBytes": 1048576, + "ConsumerPriority": 0 + } + }, + "InMemoryIntegrationTransport": { } + }, + "Endpoints": { + "DatabaseConnectionProviderBenchmarkSource": { + "GenericEndpointSettings": { + "RpcRequestSecondsTimeout": 60 + }, + "OrmSettings": { + "CommandSecondsTimeout": 10 + }, + "OutboxSettings": { + "OutboxDeliverySecondsInterval": 60 + }, + "SqlDatabaseSettings": { + "ApplicationName": "DatabaseConnectionProviderBenchmarkSource", + "Host": "localhost", + "Port": 5432, + "Database": "DatabaseConnectionProviderBenchmarkSource", + "IsolationLevel": "ReadCommitted", + "Username": "postgres", + "Password": "Password12!", + "ConnectionPoolSize": 1 + } + } + } +} \ No newline at end of file diff --git a/tests/Benchmarks/GenericHost.Benchmark/Settings/MessageHandlerMiddlewareBenchmarkSource/appsettings.json b/tests/Benchmarks/GenericHost.Benchmark/Settings/MessageHandlerMiddlewareBenchmarkSource/appsettings.json new file mode 100644 index 00000000..bf204b9c --- /dev/null +++ b/tests/Benchmarks/GenericHost.Benchmark/Settings/MessageHandlerMiddlewareBenchmarkSource/appsettings.json @@ -0,0 +1,59 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Debug", + "System": "Information", + "Microsoft": "Information", + "Npgsql": "Information" + } + }, + "Transports": { + "RabbitMqIntegrationTransport": { + "RabbitMqSettings": { + "Hosts": [ + "localhost" + ], + "Port": "5672", + "HttpApiPort": "15672", + "User": "guest", + "Password": "guest", + "VirtualHost": "MessageHandlerMiddlewareBenchmarkSource", + "ApplicationName": "MessageHandlerMiddlewareBenchmarkSource", + "ConsumerPrefetchCount": 100, + "QueueMaxLengthBytes": 1048576, + "ConsumerPriority": 0 + } + }, + "InMemoryIntegrationTransport": { } + }, + "Endpoints": { + "AuthEndpoint": { + "Authorization": { + "Issuer": "Test", + "Audience": "Test", + "PrivateKey": "db3OIsj+BXE9NZDy0t8W3TcNekrF+2d/1sFnWG4HnV8TZY30iTOdtVWJG8abWvB1GlOgJuQZdcF2Luqm/hccMw==" + } + }, + "MessageHandlerMiddlewareBenchmarkSource": { + "GenericEndpointSettings": { + "RpcRequestSecondsTimeout": 60 + }, + "OrmSettings": { + "CommandSecondsTimeout": 60 + }, + "OutboxSettings": { + "OutboxDeliverySecondsInterval": 60 + }, + "SqlDatabaseSettings": { + "ApplicationName": "MessageHandlerMiddlewareBenchmarkSource", + "Host": "localhost", + "Port": 5432, + "Database": "MessageHandlerMiddlewareBenchmarkSource", + "IsolationLevel": "ReadCommitted", + "Username": "postgres", + "Password": "Password12!", + "ConnectionPoolSize": 1 + } + } + } +} \ No newline at end of file diff --git a/tests/Benchmarks/GenericHost.Benchmark/Sources/Blog.cs b/tests/Benchmarks/GenericHost.Benchmark/Sources/Blog.cs new file mode 100644 index 00000000..3dd71663 --- /dev/null +++ b/tests/Benchmarks/GenericHost.Benchmark/Sources/Blog.cs @@ -0,0 +1,25 @@ +namespace SpaceEngineers.Core.GenericHost.Benchmark.Sources +{ + using System; + using System.Collections.Generic; + using DataAccess.Orm.Sql.Model; + using DataAccess.Orm.Sql.Model.Attributes; + + [Schema(nameof(GenericHost) + nameof(Test))] + internal record Blog : BaseDatabaseEntity + { + public Blog( + Guid primaryKey, + string theme, + IReadOnlyCollection posts) + : base(primaryKey) + { + Theme = theme; + Posts = posts; + } + + public string Theme { get; set; } + + public IReadOnlyCollection Posts { get; set; } + } +} \ No newline at end of file diff --git a/tests/Benchmarks/GenericHost.Benchmark/Sources/Community.cs b/tests/Benchmarks/GenericHost.Benchmark/Sources/Community.cs new file mode 100644 index 00000000..858e1cf6 --- /dev/null +++ b/tests/Benchmarks/GenericHost.Benchmark/Sources/Community.cs @@ -0,0 +1,25 @@ +namespace SpaceEngineers.Core.GenericHost.Benchmark.Sources +{ + using System; + using System.Collections.Generic; + using DataAccess.Orm.Sql.Model; + using DataAccess.Orm.Sql.Model.Attributes; + + [Schema(nameof(GenericHost) + nameof(Test))] + internal record Community : BaseDatabaseEntity + { + public Community( + Guid primaryKey, + string name, + IReadOnlyCollection participants) + : base(primaryKey) + { + Name = name; + Participants = participants; + } + + public string Name { get; set; } + + public IReadOnlyCollection Participants { get; set; } + } +} \ No newline at end of file diff --git a/tests/Benchmarks/GenericHost.Benchmark/Sources/ComplexDatabaseEntity.cs b/tests/Benchmarks/GenericHost.Benchmark/Sources/ComplexDatabaseEntity.cs new file mode 100644 index 00000000..3ea092aa --- /dev/null +++ b/tests/Benchmarks/GenericHost.Benchmark/Sources/ComplexDatabaseEntity.cs @@ -0,0 +1,160 @@ +namespace SpaceEngineers.Core.GenericHost.Benchmark.Sources +{ + using System; + using System.Diagnostics.CodeAnalysis; + using DataAccess.Orm.Sql.Model; + using DataAccess.Orm.Sql.Model.Attributes; + using GenericEndpoint.Contract.Abstractions; + + [SuppressMessage("Analysis", "SA1011", Justification = "space between square brackets and nullable symbol")] + [Schema(nameof(GenericHost) + nameof(Test))] + internal record ComplexDatabaseEntity : BaseDatabaseEntity + { + public ComplexDatabaseEntity(Guid primaryKey) + : base(primaryKey) + { + } + + public double Number { get; set; } + + public double? NullableNumber { get; set; } + + public Guid Identifier { get; set; } + + public Guid? NullableIdentifier { get; set; } + + public bool Boolean { get; set; } + + public bool? NullableBoolean { get; set; } + + public DateTime DateTime { get; set; } + + public DateTime? NullableDateTime { get; set; } + + public TimeSpan TimeSpan { get; set; } + + public TimeSpan? NullableTimeSpan { get; set; } + + public DateOnly DateOnly { get; set; } + + public DateOnly? NullableDateOnly { get; set; } + + public TimeOnly TimeOnly { get; set; } + + public TimeOnly? NullableTimeOnly { get; set; } + + public byte[] ByteArray { get; set; } = default!; + + public string String { get; set; } = default!; + + public string? NullableString { get; set; } + + public EnEnum Enum { get; set; } + + public EnEnum? NullableEnum { get; set; } + + public EnEnumFlags EnumFlags { get; set; } + + public EnEnumFlags? NullableEnumFlags { get; set; } + + public EnEnum[] EnumArray { get; set; } = Array.Empty(); + + public EnEnum?[] NullableEnumArray { get; set; } = Array.Empty(); + + public string[] StringArray { get; set; } = Array.Empty(); + + public string?[] NullableStringArray { get; set; } = Array.Empty(); + + public DateTime[] DateTimeArray { get; set; } = Array.Empty(); + + public DateTime?[] NullableDateTimeArray { get; set; } = Array.Empty(); + + [JsonColumn] + public IIntegrationMessage Json { get; set; } = default!; + + [JsonColumn] + public IIntegrationMessage? NullableJson { get; set; } + + [ForeignKey(EnOnDeleteBehavior.NoAction)] + public Blog Relation { get; set; } = default!; + + [ForeignKey(EnOnDeleteBehavior.NoAction)] + public Blog? NullableRelation { get; set; } + + public static ComplexDatabaseEntity Generate(IIntegrationMessage json, Blog relation) + { + return new ComplexDatabaseEntity(Guid.NewGuid()) + { + Number = 42, + NullableNumber = 42, + Identifier = Guid.NewGuid(), + NullableIdentifier = Guid.NewGuid(), + Boolean = true, + NullableBoolean = true, + DateTime = DateTime.Today, + NullableDateTime = DateTime.Today, + TimeSpan = TimeSpan.FromHours(3), + NullableTimeSpan = TimeSpan.FromHours(3), + DateOnly = DateOnly.FromDateTime(DateTime.Today), + NullableDateOnly = DateOnly.FromDateTime(DateTime.Today), + TimeOnly = TimeOnly.FromTimeSpan(TimeSpan.FromHours(3)), + NullableTimeOnly = TimeOnly.FromTimeSpan(TimeSpan.FromHours(3)), + ByteArray = new byte[] { 1, 2, 3 }, + String = "SomeString", + NullableString = "SomeNullableString", + Enum = EnEnum.Three, + NullableEnum = EnEnum.Three, + EnumFlags = EnEnumFlags.A | EnEnumFlags.B, + NullableEnumFlags = EnEnumFlags.A | EnEnumFlags.B, + EnumArray = new[] { EnEnum.One, EnEnum.Two, EnEnum.Three }, + NullableEnumArray = new EnEnum?[] { EnEnum.One, EnEnum.Two, EnEnum.Three }, + StringArray = new[] { "SomeString", "AnotherString" }, + NullableStringArray = new[] { "SomeString", "AnotherString" }, + DateTimeArray = new[] { DateTime.MaxValue, DateTime.MaxValue }, + NullableDateTimeArray = new DateTime?[] { DateTime.MaxValue, DateTime.MaxValue }, + Json = json, + NullableJson = json, + Relation = relation, + NullableRelation = relation + }; + } + + public static ComplexDatabaseEntity GenerateWithNulls(IIntegrationMessage json, Blog relation) + { + return new ComplexDatabaseEntity(Guid.NewGuid()) + { + Number = 42, + NullableNumber = null, + Identifier = Guid.NewGuid(), + NullableIdentifier = null, + Boolean = true, + NullableBoolean = null, + DateTime = DateTime.Today, + NullableDateTime = null, + TimeSpan = TimeSpan.FromHours(3), + NullableTimeSpan = null, + DateOnly = DateOnly.FromDateTime(DateTime.Today), + NullableDateOnly = null, + TimeOnly = TimeOnly.FromTimeSpan(TimeSpan.FromHours(3)), + NullableTimeOnly = null, + ByteArray = new byte[] { 1, 2, 3 }, + String = "SomeString", + NullableString = null, + Enum = EnEnum.Three, + NullableEnum = null, + EnumFlags = EnEnumFlags.A | EnEnumFlags.B, + NullableEnumFlags = null, + EnumArray = new[] { EnEnum.One, EnEnum.Two, EnEnum.Three }, + NullableEnumArray = new EnEnum?[] { EnEnum.One, null, EnEnum.Three }, + StringArray = new[] { "SomeString", "AnotherString" }, + NullableStringArray = new[] { "SomeString", null, "AnotherString" }, + DateTimeArray = new[] { DateTime.MaxValue, DateTime.MaxValue }, + NullableDateTimeArray = new DateTime?[] { DateTime.MaxValue, null, DateTime.MaxValue }, + Json = json, + NullableJson = null, + Relation = relation, + NullableRelation = null + }; + } + } +} \ No newline at end of file diff --git a/tests/Benchmarks/GenericHost.Benchmark/Sources/DatabaseConnectionProviderBenchmarkSource.cs b/tests/Benchmarks/GenericHost.Benchmark/Sources/DatabaseConnectionProviderBenchmarkSource.cs new file mode 100644 index 00000000..8e854fba --- /dev/null +++ b/tests/Benchmarks/GenericHost.Benchmark/Sources/DatabaseConnectionProviderBenchmarkSource.cs @@ -0,0 +1,249 @@ +namespace SpaceEngineers.Core.GenericHost.Benchmark.Sources +{ + using System; + using System.Collections.Generic; + using System.Diagnostics.CodeAnalysis; + using System.Linq; + using System.Reflection; + using System.Threading; + using System.Threading.Tasks; + using Basics; + using BenchmarkDotNet.Attributes; + using BenchmarkDotNet.Engines; + using CompositionRoot; + using DataAccess.Orm.Sql.Linq; + using DataAccess.Orm.Sql.Model; + using DataAccess.Orm.Sql.Transaction; + using GenericEndpoint.Contract; + using GenericEndpoint.DataAccess.Sql.Postgres.Host; + using GenericEndpoint.Host; + using IntegrationTransport.Host; + using Test.Api.ClassFixtures; + using IHost = Microsoft.Extensions.Hosting.IHost; + + /// + /// IDatabaseConnectionProvider query benchmark source + /// + [SimpleJob(RunStrategy.Throughput)] + [SuppressMessage("Analysis", "CA1506", Justification = "application composition root")] + [SuppressMessage("Analysis", "CA1001", Justification = "benchmark source")] + public class DatabaseConnectionProviderBenchmarkSource + { + private CancellationTokenSource? _cts; + private IHost? _host; + private IDependencyContainer? _dependencyContainer; + + /// + /// GlobalSetup + /// + [GlobalSetup] + public void GlobalSetup() + { + _cts = new CancellationTokenSource(TimeSpan.FromMinutes(5)); + + var solutionFileDirectory = SolutionExtensions.SolutionFile().Directory + ?? throw new InvalidOperationException("Solution directory wasn't found"); + + var settingsDirectory = solutionFileDirectory + .StepInto("tests") + .StepInto("Benchmarks") + .StepInto("GenericHost.Benchmark") + .StepInto("Settings") + .StepInto(nameof(DatabaseConnectionProviderBenchmarkSource)); + + var databaseEntities = new[] + { + typeof(ComplexDatabaseEntity), + typeof(Blog), + typeof(Post), + typeof(User), + typeof(Community), + typeof(Participant) + }; + + var startupActions = new[] + { + typeof(RecreatePostgreSqlDatabaseHostedServiceStartupAction) + }; + + var additionalOurTypes = databaseEntities + .Concat(startupActions) + .ToArray(); + + var transportIdentity = IntegrationTransport.InMemory.Identity.TransportIdentity(); + + var endpointIdentity = new EndpointIdentity( + nameof(DatabaseConnectionProviderBenchmarkSource), + Assembly.GetEntryAssembly() ?? throw new InvalidOperationException("Unable to get entry assembly")); + + _host = new TestFixture() + .CreateHostBuilder() + .UseInMemoryIntegrationTransport(transportIdentity) + .UseEndpoint(endpointIdentity, + builder => builder + .WithPostgreSqlDataAccess(options => options + .ExecuteMigrations()) + .ModifyContainerOptions(options => options + .WithAdditionalOurTypes(additionalOurTypes)) + .BuildOptions()) + .BuildHost(settingsDirectory); + + _host.StartAsync(_cts.Token).Wait(_cts.Token); + + _dependencyContainer = _host.GetEndpointDependencyContainer(endpointIdentity); + } + + /// + /// GlobalCleanup + /// + [GlobalCleanup] + public void GlobalCleanup() + { + _host.StopAsync(_cts.Token).Wait(); + _host.Dispose(); + _cts.Dispose(); + } + + /// + /// IterationSetup + /// + [IterationSetup] + public void IterationSetup() + { + Insert().Wait(_cts.Token); + } + + /// + /// IterationCleanup + /// + [IterationCleanup] + public void IterationCleanup() + { + Delete().Wait(_cts.Token); + } + + /// Read + /// Ongoing operation + [Benchmark(Description = nameof(Read), Baseline = true)] + public async Task Read() + { + await _dependencyContainer! + .InvokeWithinTransaction(true, Producer, _cts.Token) + .ConfigureAwait(false); + + static Task Producer( + IDatabaseContext transaction, + CancellationToken token) + { + return transaction + .All() + .CachedExpression("ECF915DD-4CDA-4B6C-ACF5-E497B418D813") + .ToListAsync(token); + } + } + + /// Insert + /// Ongoing operation + [Benchmark(Description = nameof(Insert))] + public async Task Insert() + { + var user = new User(Guid.NewGuid(), "SpaceEngineer"); + var posts = new List(); + var blog = new Blog(Guid.NewGuid(), "MilkyWay", posts); + var post = new Post(Guid.NewGuid(), blog, user, DateTime.Now, "PostContent"); + posts.Add(post); + + var communities = new List(); + var participants = new List(); + var community = new Community(Guid.NewGuid(), "AmazingCommunity", participants); + var participant = new Participant(Guid.NewGuid(), "RegularParticipant", communities); + communities.Add(community); + participants.Add(participant); + + var message = new Request(); + var complexDatabaseEntity = ComplexDatabaseEntity.Generate(message, blog); + + var entities = new IDatabaseEntity[] + { + user, + blog, + post, + complexDatabaseEntity, + community, + participant + }; + + await _dependencyContainer! + .InvokeWithinTransaction(true, entities, Producer, _cts.Token) + .ConfigureAwait(false); + + static Task Producer( + IDatabaseContext transaction, + IDatabaseEntity[] entities, + CancellationToken token) + { + return transaction + .Insert(entities, EnInsertBehavior.Default) + .CachedExpression("22046C75-F23F-4C21-B143-A98687105410") + .Invoke(token); + } + } + + /// Delete + /// Ongoing operation + [Benchmark(Description = nameof(Delete))] + public async Task Delete() + { + await _dependencyContainer! + .InvokeWithinTransaction(true, Producer, _cts.Token) + .ConfigureAwait(false); + + static async Task Producer( + IDatabaseContext transaction, + CancellationToken token) + { + await transaction + .Delete() + .Where(_ => true) + .CachedExpression("14CBC111-F1B3-41AB-A816-47BF6C88B4EF") + .Invoke(token) + .ConfigureAwait(false); + + await transaction + .Delete() + .Where(_ => true) + .CachedExpression("14A30D13-91D1-4CDA-8500-5F36C625B2F5") + .Invoke(token) + .ConfigureAwait(false); + + await transaction + .Delete() + .Where(_ => true) + .CachedExpression("C9A9FAA9-911E-4E7A-AF1F-34E16C36C168") + .Invoke(token) + .ConfigureAwait(false); + + await transaction + .Delete() + .Where(_ => true) + .CachedExpression("D60B12D2-B738-4E86-BCAA-AE5D701FF8C0") + .Invoke(token) + .ConfigureAwait(false); + + await transaction + .Delete() + .Where(_ => true) + .CachedExpression("9379254D-97D6-4A96-854C-237220E4DD1E") + .Invoke(token) + .ConfigureAwait(false); + + await transaction + .Delete() + .Where(_ => true) + .CachedExpression("248F4251-18A0-400B-BC94-62348F78DA71") + .Invoke(token) + .ConfigureAwait(false); + } + } + } +} \ No newline at end of file diff --git a/tests/Benchmarks/GenericHost.Benchmark/Sources/EnEnum.cs b/tests/Benchmarks/GenericHost.Benchmark/Sources/EnEnum.cs new file mode 100644 index 00000000..39b6900f --- /dev/null +++ b/tests/Benchmarks/GenericHost.Benchmark/Sources/EnEnum.cs @@ -0,0 +1,20 @@ +namespace SpaceEngineers.Core.GenericHost.Benchmark.Sources +{ + internal enum EnEnum + { + /// + /// One + /// + One = 1, + + /// + /// Two + /// + Two = 2, + + /// + /// Three + /// + Three = 4 + } +} \ No newline at end of file diff --git a/tests/Benchmarks/GenericHost.Benchmark/Sources/EnEnumFlags.cs b/tests/Benchmarks/GenericHost.Benchmark/Sources/EnEnumFlags.cs new file mode 100644 index 00000000..e4bb05e5 --- /dev/null +++ b/tests/Benchmarks/GenericHost.Benchmark/Sources/EnEnumFlags.cs @@ -0,0 +1,23 @@ +namespace SpaceEngineers.Core.GenericHost.Benchmark.Sources +{ + using System; + + [Flags] + internal enum EnEnumFlags + { + /// + /// A + /// + A = 1 << 0, + + /// + /// B + /// + B = 1 << 1, + + /// + /// c + /// + C = 1 << 2 + } +} \ No newline at end of file diff --git a/tests/Benchmarks/GenericHost.Benchmark/Sources/MessageHandlerMiddlewareBenchmarkSource.cs b/tests/Benchmarks/GenericHost.Benchmark/Sources/MessageHandlerMiddlewareBenchmarkSource.cs new file mode 100644 index 00000000..7ae0ebf7 --- /dev/null +++ b/tests/Benchmarks/GenericHost.Benchmark/Sources/MessageHandlerMiddlewareBenchmarkSource.cs @@ -0,0 +1,270 @@ +namespace SpaceEngineers.Core.GenericHost.Benchmark.Sources +{ + using System; + using System.Collections.Generic; + using System.Diagnostics.CodeAnalysis; + using System.Linq; + using System.Reflection; + using System.Threading; + using System.Threading.Tasks; + using Basics; + using BenchmarkDotNet.Attributes; + using BenchmarkDotNet.Engines; + using CompositionRoot; + using GenericEndpoint.Authorization; + using GenericEndpoint.Authorization.Host; + using GenericEndpoint.Contract; + using GenericEndpoint.DataAccess.Sql.Postgres.Host; + using GenericEndpoint.EventSourcing.Host; + using GenericEndpoint.Host; + using GenericEndpoint.Messaging; + using GenericEndpoint.Messaging.MessageHeaders; + using GenericEndpoint.Pipeline; + using GenericEndpoint.Telemetry; + using GenericEndpoint.Telemetry.Host; + using IntegrationTransport.Host; + using IntegrationTransport.RabbitMQ; + using JwtAuthentication; + using Test.Api.ClassFixtures; + using IHost = Microsoft.Extensions.Hosting.IHost; + + /// + /// IMessageHandlerMiddleware benchmark source + /// + [SimpleJob(RunStrategy.Throughput)] + [SuppressMessage("Analysis", "CA1506", Justification = "application composition root")] + [SuppressMessage("Analysis", "CA1001", Justification = "benchmark source")] + public class MessageHandlerMiddlewareBenchmarkSource + { + private CancellationTokenSource? _cts; + private IHost? _host; + private IntegrationMessage? _request; + private Func? _messageHandler; + private IDependencyContainer? _dependencyContainer; + private IMessageHandlerMiddlewareComposite? _messageHandlerMiddleware; + private IMessageHandlerMiddleware? _tracingMiddleware; + private IMessageHandlerMiddleware? _errorHandlingMiddleware; + private IMessageHandlerMiddleware? _authorizationMiddleware; + private IMessageHandlerMiddleware? _unitOfWorkMiddleware; + private IMessageHandlerMiddleware? _handledByEndpointMiddleware; + private IMessageHandlerMiddleware? _requestReplyMiddleware; + + /// + /// GlobalSetup + /// + [GlobalSetup] + public void GlobalSetup() + { + _cts = new CancellationTokenSource(TimeSpan.FromMinutes(5)); + + var solutionFileDirectory = SolutionExtensions.SolutionFile().Directory + ?? throw new InvalidOperationException("Solution directory wasn't found"); + + var settingsDirectory = solutionFileDirectory + .StepInto("tests") + .StepInto("Benchmarks") + .StepInto("GenericHost.Benchmark") + .StepInto("Settings") + .StepInto(nameof(MessageHandlerMiddlewareBenchmarkSource)); + + var transportIdentity = IntegrationTransport.InMemory.Identity.TransportIdentity(); + + var endpointIdentity = new EndpointIdentity( + nameof(MessageHandlerMiddlewareBenchmarkSource), + Assembly.GetEntryAssembly() ?? throw new InvalidOperationException("Unable to get entry assembly")); + + _host = new TestFixture() + .CreateHostBuilder() + .UseInMemoryIntegrationTransport(transportIdentity) + .UseEndpoint(endpointIdentity, + builder => builder + .WithPostgreSqlDataAccess(options => options + .ExecuteMigrations()) + .WithSqlEventSourcing() + .WithJwtAuthentication(builder.Context.Configuration) + .WithAuthorization() + .WithOpenTelemetry() + .ModifyContainerOptions(options => options + .WithAdditionalOurTypes(typeof(RecreatePostgreSqlDatabaseHostedServiceStartupAction))) + .BuildOptions()) + .UseOpenTelemetry() + .BuildHost(settingsDirectory); + + _host.StartAsync(_cts.Token).Wait(_cts.Token); + + _dependencyContainer = _host.GetEndpointDependencyContainer(endpointIdentity); + + var username = "qwerty"; + + var authorizationToken = _dependencyContainer + .Resolve() + .GenerateToken( + username, + new[] + { + nameof(MessageHandlerMiddlewareBenchmarkSource) + }, + TimeSpan.FromSeconds(300)); + + _request = _dependencyContainer + .Resolve() + .CreateGeneralMessage( + new Request(), + typeof(Request), + new[] { new Authorization(authorizationToken) }, + null); + + _messageHandler = static (context, token) => context.Reply((Request)context.Message.Payload, new Reply(), token); + + _messageHandlerMiddleware = _dependencyContainer.Resolve(); + + var middlewares = _dependencyContainer.ResolveCollection().ToList(); + + _tracingMiddleware = middlewares.Single(middleware => middleware.GetType() == typeof(TracingMiddleware)); + _errorHandlingMiddleware = middlewares.Single(middleware => middleware.GetType() == typeof(ErrorHandlingMiddleware)); + _authorizationMiddleware = middlewares.Single(middleware => middleware.GetType() == typeof(AuthorizationMiddleware)); + _unitOfWorkMiddleware = middlewares.Single(middleware => middleware.GetType() == typeof(UnitOfWorkMiddleware)); + _handledByEndpointMiddleware = middlewares.Single(middleware => middleware.GetType() == typeof(HandledByEndpointMiddleware)); + _requestReplyMiddleware = middlewares.Single(middleware => middleware.GetType() == typeof(RequestReplyMiddleware)); + } + + /// + /// GlobalCleanup + /// + [GlobalCleanup] + public void GlobalCleanup() + { + _host.StopAsync(_cts.Token).Wait(); + _host.Dispose(); + _cts.Dispose(); + } + + /// + /// IterationSetup + /// + [IterationSetup] + public void IterationSetup() + { + _ = _dependencyContainer; + } + + /// + /// IterationCleanup + /// + [IterationCleanup] + public void IterationCleanup() + { + var headers = (Dictionary)_request.Headers; + + _ = headers.Remove(typeof(ActualDeliveryDate)); + _ = headers.Remove(typeof(DeliveryTag)); + _ = headers.Remove(typeof(HandledBy)); + _ = headers.Remove(typeof(RejectReason)); + } + + /// RunCompositeMiddleware + /// Ongoing operation + [Benchmark(Description = nameof(RunCompositeMiddleware), Baseline = true)] + public async Task RunCompositeMiddleware() + { + await using (_dependencyContainer.OpenScopeAsync().ConfigureAwait(false)) + { + var exclusiveContext = _dependencyContainer.Resolve(_request!); + + await _messageHandlerMiddleware + .Handle(exclusiveContext, _messageHandler!, _cts.Token) + .ConfigureAwait(false); + } + } + + /// RunTracingMiddleware + /// Ongoing operation + [Benchmark(Description = nameof(RunTracingMiddleware))] + public async Task RunTracingMiddleware() + { + await using (_dependencyContainer.OpenScopeAsync().ConfigureAwait(false)) + { + var exclusiveContext = _dependencyContainer.Resolve(_request!); + + await _tracingMiddleware + .Handle(exclusiveContext, _messageHandler!, _cts.Token) + .ConfigureAwait(false); + } + } + + /// RunErrorHandlingMiddleware + /// Ongoing operation + [Benchmark(Description = nameof(RunErrorHandlingMiddleware))] + public async Task RunErrorHandlingMiddleware() + { + await using (_dependencyContainer.OpenScopeAsync().ConfigureAwait(false)) + { + var exclusiveContext = _dependencyContainer.Resolve(_request!); + + await _errorHandlingMiddleware + .Handle(exclusiveContext, _messageHandler!, _cts.Token) + .ConfigureAwait(false); + } + } + + /// RunAuthorizationMiddleware + /// Ongoing operation + [Benchmark(Description = nameof(RunAuthorizationMiddleware))] + public async Task RunAuthorizationMiddleware() + { + await using (_dependencyContainer.OpenScopeAsync().ConfigureAwait(false)) + { + var exclusiveContext = _dependencyContainer.Resolve(_request!); + + await _authorizationMiddleware + .Handle(exclusiveContext, _messageHandler!, _cts.Token) + .ConfigureAwait(false); + } + } + + /// RunUnitOfWorkMiddleware + /// Ongoing operation + [Benchmark(Description = nameof(RunUnitOfWorkMiddleware))] + public async Task RunUnitOfWorkMiddleware() + { + await using (_dependencyContainer.OpenScopeAsync().ConfigureAwait(false)) + { + var exclusiveContext = _dependencyContainer.Resolve(_request!); + + await _unitOfWorkMiddleware + .Handle(exclusiveContext, _messageHandler!, _cts.Token) + .ConfigureAwait(false); + } + } + + /// RunHandledByEndpointMiddleware + /// Ongoing operation + [Benchmark(Description = nameof(RunHandledByEndpointMiddleware))] + public async Task RunHandledByEndpointMiddleware() + { + await using (_dependencyContainer.OpenScopeAsync().ConfigureAwait(false)) + { + var exclusiveContext = _dependencyContainer.Resolve(_request!); + + await _handledByEndpointMiddleware + .Handle(exclusiveContext, _messageHandler!, _cts.Token) + .ConfigureAwait(false); + } + } + + /// RunRequestReplyMiddleware + /// Ongoing operation + [Benchmark(Description = nameof(RunRequestReplyMiddleware))] + public async Task RunRequestReplyMiddleware() + { + await using (_dependencyContainer.OpenScopeAsync().ConfigureAwait(false)) + { + var exclusiveContext = _dependencyContainer.Resolve(_request!); + + await _requestReplyMiddleware + .Handle(exclusiveContext, _messageHandler!, _cts.Token) + .ConfigureAwait(false); + } + } + } +} \ No newline at end of file diff --git a/tests/Benchmarks/GenericHost.Benchmark/Sources/Participant.cs b/tests/Benchmarks/GenericHost.Benchmark/Sources/Participant.cs new file mode 100644 index 00000000..63a38a47 --- /dev/null +++ b/tests/Benchmarks/GenericHost.Benchmark/Sources/Participant.cs @@ -0,0 +1,25 @@ +namespace SpaceEngineers.Core.GenericHost.Benchmark.Sources +{ + using System; + using System.Collections.Generic; + using DataAccess.Orm.Sql.Model; + using DataAccess.Orm.Sql.Model.Attributes; + + [Schema(nameof(GenericHost) + nameof(Test))] + internal record Participant : BaseDatabaseEntity + { + public Participant( + Guid primaryKey, + string name, + IReadOnlyCollection communities) + : base(primaryKey) + { + Name = name; + Communities = communities; + } + + public string Name { get; set; } + + public IReadOnlyCollection Communities { get; set; } + } +} \ No newline at end of file diff --git a/tests/Benchmarks/GenericHost.Benchmark/Sources/Post.cs b/tests/Benchmarks/GenericHost.Benchmark/Sources/Post.cs new file mode 100644 index 00000000..cef60a48 --- /dev/null +++ b/tests/Benchmarks/GenericHost.Benchmark/Sources/Post.cs @@ -0,0 +1,34 @@ +namespace SpaceEngineers.Core.GenericHost.Benchmark.Sources +{ + using System; + using DataAccess.Orm.Sql.Model; + using DataAccess.Orm.Sql.Model.Attributes; + + [Schema(nameof(GenericHost) + nameof(Test))] + internal record Post : BaseDatabaseEntity + { + public Post( + Guid primaryKey, + Blog blog, + User user, + DateTime dateTime, + string text) + : base(primaryKey) + { + Blog = blog; + User = user; + DateTime = dateTime; + Text = text; + } + + [ForeignKey(EnOnDeleteBehavior.Cascade)] + public Blog Blog { get; set; } + + [ForeignKey(EnOnDeleteBehavior.Restrict)] + public User User { get; set; } + + public DateTime DateTime { get; set; } + + public string Text { get; set; } + } +} \ No newline at end of file diff --git a/tests/Benchmarks/GenericHost.Benchmark/Sources/RecreatePostgreSqlDatabaseHostedServiceStartupAction.cs b/tests/Benchmarks/GenericHost.Benchmark/Sources/RecreatePostgreSqlDatabaseHostedServiceStartupAction.cs new file mode 100644 index 00000000..138e6ee1 --- /dev/null +++ b/tests/Benchmarks/GenericHost.Benchmark/Sources/RecreatePostgreSqlDatabaseHostedServiceStartupAction.cs @@ -0,0 +1,98 @@ +namespace SpaceEngineers.Core.GenericHost.Benchmark.Sources +{ + using System; + using System.Diagnostics.CodeAnalysis; + using System.Threading; + using System.Threading.Tasks; + using AutoRegistration.Api.Abstractions; + using AutoRegistration.Api.Attributes; + using AutoRegistration.Api.Enumerations; + using Basics; + using CompositionRoot; + using CrossCuttingConcerns.Settings; + using DataAccess.Orm.Sql.Connection; + using DataAccess.Orm.Sql.Settings; + using DataAccess.Orm.Sql.Translation; + using Npgsql; + + [Component(EnLifestyle.Singleton)] + internal class RecreatePostgreSqlDatabaseHostedServiceStartupAction : IHostedServiceStartupAction, + ICollectionResolvable, + ICollectionResolvable, + IResolvable + { + private const string CommandText = @"create extension if not exists dblink; + +drop database if exists ""{0}"" with (FORCE); +create database ""{0}""; +grant all privileges on database ""{0}"" to ""{1}"";"; + + private readonly SqlDatabaseSettings _sqlDatabaseSettings; + private readonly IDependencyContainer _dependencyContainer; + private readonly IDatabaseConnectionProvider _connectionProvider; + + public RecreatePostgreSqlDatabaseHostedServiceStartupAction( + ISettingsProvider sqlDatabaseSettingsProvider, + IDependencyContainer dependencyContainer, + IDatabaseConnectionProvider connectionProvider) + { + _sqlDatabaseSettings = sqlDatabaseSettingsProvider.Get(); + + _dependencyContainer = dependencyContainer; + _connectionProvider = connectionProvider; + } + + [SuppressMessage("Analysis", "CA2000", Justification = "IDbConnection will be disposed in outer scope by client")] + public async Task Run(CancellationToken token) + { + var command = new SqlCommand( + CommandText.Format(_sqlDatabaseSettings.Database, _sqlDatabaseSettings.Username), + Array.Empty()); + + var connectionStringBuilder = new NpgsqlConnectionStringBuilder + { + Host = _sqlDatabaseSettings.Host, + Port = _sqlDatabaseSettings.Port, + Database = "postgres", + Username = _sqlDatabaseSettings.Username, + Password = _sqlDatabaseSettings.Password + }; + + var npgSqlConnection = new NpgsqlConnection(connectionStringBuilder.ConnectionString); + + try + { + await npgSqlConnection.OpenAsync(token).ConfigureAwait(false); + + _ = await _connectionProvider + .Execute(npgSqlConnection, command, token) + .ConfigureAwait(false); + } + finally + { + npgSqlConnection.Dispose(); + } + + NpgsqlConnection.ClearPool(npgSqlConnection); + + while (true) + { + var doesDatabaseExist = await _dependencyContainer + .Resolve() + .DoesDatabaseExist(token) + .ConfigureAwait(false); + + if (!doesDatabaseExist) + { + await Task + .Delay(TimeSpan.FromMilliseconds(100), token) + .ConfigureAwait(false); + } + else + { + break; + } + } + } + } +} \ No newline at end of file diff --git a/tests/Benchmarks/GenericHost.Benchmark/Sources/Reply.cs b/tests/Benchmarks/GenericHost.Benchmark/Sources/Reply.cs new file mode 100644 index 00000000..346578e0 --- /dev/null +++ b/tests/Benchmarks/GenericHost.Benchmark/Sources/Reply.cs @@ -0,0 +1,13 @@ +namespace SpaceEngineers.Core.GenericHost.Benchmark.Sources +{ + using GenericEndpoint.Contract.Abstractions; + using GenericEndpoint.Contract.Attributes; + + /// + /// Reply + /// + [Feature(nameof(MessageHandlerMiddlewareBenchmarkSource))] + public record Reply : IIntegrationReply + { + } +} \ No newline at end of file diff --git a/tests/Benchmarks/GenericHost.Benchmark/Sources/Request.cs b/tests/Benchmarks/GenericHost.Benchmark/Sources/Request.cs new file mode 100644 index 00000000..5340b045 --- /dev/null +++ b/tests/Benchmarks/GenericHost.Benchmark/Sources/Request.cs @@ -0,0 +1,14 @@ +namespace SpaceEngineers.Core.GenericHost.Benchmark.Sources +{ + using GenericEndpoint.Contract.Abstractions; + using GenericEndpoint.Contract.Attributes; + + /// + /// Request + /// + [OwnedBy(nameof(MessageHandlerMiddlewareBenchmarkSource))] + [Feature(nameof(MessageHandlerMiddlewareBenchmarkSource))] + public record Request : IIntegrationRequest + { + } +} \ No newline at end of file diff --git a/tests/Benchmarks/GenericHost.Benchmark/Sources/User.cs b/tests/Benchmarks/GenericHost.Benchmark/Sources/User.cs new file mode 100644 index 00000000..ceffa56a --- /dev/null +++ b/tests/Benchmarks/GenericHost.Benchmark/Sources/User.cs @@ -0,0 +1,18 @@ +namespace SpaceEngineers.Core.GenericHost.Benchmark.Sources +{ + using System; + using DataAccess.Orm.Sql.Model; + using DataAccess.Orm.Sql.Model.Attributes; + + [Schema(nameof(GenericHost) + nameof(Test))] + internal record User : BaseDatabaseEntity + { + public User(Guid primaryKey, string nickname) + : base(primaryKey) + { + Nickname = nickname; + } + + public string Nickname { get; set; } + } +} \ No newline at end of file diff --git a/tests/Benchmarks/Modules.Benchmark/Benchmarks.cs b/tests/Benchmarks/Modules.Benchmark/Benchmarks.cs new file mode 100644 index 00000000..406ea903 --- /dev/null +++ b/tests/Benchmarks/Modules.Benchmark/Benchmarks.cs @@ -0,0 +1,92 @@ +namespace SpaceEngineers.Core.Modules.Benchmark +{ + using Core.Benchmark.Api; + using Sources; + using Test.Api; + using Test.Api.ClassFixtures; + using Xunit; + using Xunit.Abstractions; + + /// + /// Benchmarks + /// + // TODO: #136 - remove magic numbers and use adaptive approach -> store test artifacts in DB and search performance change points + public class Benchmarks : TestBase + { + /// .cctor + /// ITestOutputHelper + /// TestFixture + public Benchmarks(ITestOutputHelper output, TestFixture fixture) + : base(output, fixture) + { + } + + [Fact(Timeout = 300_000)] + internal void DeepCopyBenchmark() + { + var summary = Benchmark.Run(Output.WriteLine); + + var bySerialization = summary.MillisecondMeasure( + nameof(DeepCopyBenchmarkSource.DeepCopyBySerialization), + Measure.Mean, + Output.WriteLine); + + var byReflection = summary.MillisecondMeasure( + nameof(DeepCopyBenchmarkSource.DeepCopyByReflection), + Measure.Mean, + Output.WriteLine); + + var multiplier = bySerialization / byReflection; + Output.WriteLine($"{nameof(multiplier)}: {multiplier:N}"); + + Assert.True(bySerialization <= 100m); + Assert.True(byReflection <= 50m); + } + + [Fact(Timeout = 300_000)] + internal void AssembliesExtensionsBelowBenchmark() + { + var summary = Benchmark.Run(Output.WriteLine); + + var measure = summary.MillisecondMeasure( + nameof(AssembliesExtensionsBelowBenchmarkSource.Below), + Measure.Mean, + Output.WriteLine); + + Assert.True(measure <= 25m); + } + + [Fact(Timeout = 300_000)] + internal void CompositionRootStartupBenchmark() + { + var summary = Benchmark.Run(Output.WriteLine); + + var create = summary.MillisecondMeasure( + nameof(CompositionRootStartupBenchmarkSource.Create), + Measure.Mean, + Output.WriteLine); + + Assert.True(create <= 1000m); + } + + [Fact(Timeout = 300_000)] + internal void StreamCopyVersusStreamReadBenchmark() + { + var summary = Benchmark.Run(Output.WriteLine); + + var copyTo = summary.MillisecondMeasure( + nameof(StreamCopyVersusStreamReadBenchmarkSource.CopyTo), + Measure.Mean, + Output.WriteLine); + + Assert.True(copyTo <= 10m); + + var read = summary.MillisecondMeasure( + nameof(StreamCopyVersusStreamReadBenchmarkSource.Read), + Measure.Mean, + Output.WriteLine); + + Assert.True(read <= 10m); + } + } +} \ No newline at end of file diff --git a/tests/Benchmarks/Modules.Benchmark/Modules.Benchmark.csproj b/tests/Benchmarks/Modules.Benchmark/Modules.Benchmark.csproj new file mode 100644 index 00000000..6e5ef324 --- /dev/null +++ b/tests/Benchmarks/Modules.Benchmark/Modules.Benchmark.csproj @@ -0,0 +1,38 @@ + + + + net7.0 + Modules.Benchmark + SpaceEngineers.Core.Modules.Benchmark + false + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + + + + + + + + + diff --git a/tests/Benchmarks/Modules.Benchmark/Properties/AssemblyInfo.cs b/tests/Benchmarks/Modules.Benchmark/Properties/AssemblyInfo.cs new file mode 100644 index 00000000..31adb06d --- /dev/null +++ b/tests/Benchmarks/Modules.Benchmark/Properties/AssemblyInfo.cs @@ -0,0 +1,5 @@ +using System.Reflection; + +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] +[assembly: AssemblyInformationalVersion("1.0.0.0")] \ No newline at end of file diff --git a/tests/Benchmarks/Modules.Benchmark/Settings/appsettings.json b/tests/Benchmarks/Modules.Benchmark/Settings/appsettings.json new file mode 100644 index 00000000..7086180d --- /dev/null +++ b/tests/Benchmarks/Modules.Benchmark/Settings/appsettings.json @@ -0,0 +1,10 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "System": "Information", + "Microsoft": "Information", + "Npgsql": "Information" + } + } +} diff --git a/tests/Benchmarks/Modules.Benchmark/Sources/AssembliesExtensionsBelowBenchmarkSource.cs b/tests/Benchmarks/Modules.Benchmark/Sources/AssembliesExtensionsBelowBenchmarkSource.cs new file mode 100644 index 00000000..81a7f26a --- /dev/null +++ b/tests/Benchmarks/Modules.Benchmark/Sources/AssembliesExtensionsBelowBenchmarkSource.cs @@ -0,0 +1,39 @@ +namespace SpaceEngineers.Core.Modules.Benchmark.Sources +{ + using System; + using System.Diagnostics.CodeAnalysis; + using System.Reflection; + using Basics; + using BenchmarkDotNet.Attributes; + using BenchmarkDotNet.Engines; + + /// + /// AssembliesExtensions.Below(assembly) benchmark source + /// + [SimpleJob(RunStrategy.Throughput)] + public class AssembliesExtensionsBelowBenchmarkSource + { + [SuppressMessage("Analysis", "SA1011", Justification = "space between square brackets and nullable symbol")] + private Assembly[]? _allAssemblies; + private Assembly? _belowAssembly; + + private Assembly[] AllAssemblies => _allAssemblies ?? throw new InvalidOperationException(nameof(_allAssemblies)); + + private Assembly BelowAssembly => _belowAssembly ?? throw new InvalidOperationException(nameof(_belowAssembly)); + + /// + /// GlobalSetup + /// + [GlobalSetup] + public void GlobalSetup() + { + _allAssemblies = AssembliesExtensions.AllAssembliesFromCurrentDomain(); + _belowAssembly = GetType().Assembly; + } + + /// AssembliesExtensions.Below + /// Below assemblies + [Benchmark(Description = nameof(Below), Baseline = true)] + public Assembly[] Below() => AllAssemblies.Below(BelowAssembly); + } +} \ No newline at end of file diff --git a/tests/Benchmarks/Modules.Benchmark/Sources/CompositionRootStartupBenchmarkSource.cs b/tests/Benchmarks/Modules.Benchmark/Sources/CompositionRootStartupBenchmarkSource.cs new file mode 100644 index 00000000..700eeb03 --- /dev/null +++ b/tests/Benchmarks/Modules.Benchmark/Sources/CompositionRootStartupBenchmarkSource.cs @@ -0,0 +1,68 @@ +namespace SpaceEngineers.Core.Modules.Benchmark.Sources +{ + using System; + using System.Diagnostics.CodeAnalysis; + using System.IO; + using System.Reflection; + using Basics; + using BenchmarkDotNet.Attributes; + using BenchmarkDotNet.Engines; + using CompositionRoot; + using CrossCuttingConcerns.Settings; + + /// + /// Creates DependencyContainer and measures cold start time + /// + [SimpleJob(RunStrategy.ColdStart)] + public class CompositionRootStartupBenchmarkSource + { + private DirectoryInfo? _settingsDirectory; + + [SuppressMessage("Analysis", "SA1011", Justification = "space between square brackets and nullable symbol")] + private Assembly[]? _assemblies; + + private Assembly[] Assemblies => _assemblies ?? throw new InvalidOperationException(nameof(_assemblies)); + + /// + /// GlobalSetup + /// + [GlobalSetup] + public void GlobalSetup() + { + var solutionFileDirectory = SolutionExtensions.SolutionFile().Directory + ?? throw new InvalidOperationException("Solution directory wasn't found"); + + _settingsDirectory = solutionFileDirectory + .StepInto("tests") + .StepInto("Benchmarks") + .StepInto("Modules.Benchmark") + .StepInto("Settings"); + + _assemblies = new[] + { + AssembliesExtensions.FindRequiredAssembly(AssembliesExtensions.BuildName(nameof(SpaceEngineers), nameof(Core), nameof(CrossCuttingConcerns))), + + AssembliesExtensions.FindRequiredAssembly(AssembliesExtensions.BuildName(nameof(SpaceEngineers), nameof(Core), nameof(DataImport))), + AssembliesExtensions.FindRequiredAssembly(AssembliesExtensions.BuildName(nameof(SpaceEngineers), nameof(Core), nameof(DataExport))), + + AssembliesExtensions.FindRequiredAssembly(AssembliesExtensions.BuildName(nameof(SpaceEngineers), nameof(Core), nameof(Dynamic))), + + AssembliesExtensions.FindRequiredAssembly(AssembliesExtensions.BuildName(nameof(SpaceEngineers), nameof(Core), nameof(CliArgumentsParser))), + + AssembliesExtensions.FindRequiredAssembly("System.Private.CoreLib") + }; + } + + /// CreateExactlyBounded + /// IDependencyContainer + [Benchmark(Description = nameof(Create))] + public IDependencyContainer Create() + { + var options = new DependencyContainerOptions() + .WithPluginAssemblies(Assemblies) + .WithManualRegistrations(new SettingsDirectoryProviderManualRegistration(new SettingsDirectoryProvider(_settingsDirectory!))); + + return DependencyContainer.Create(options); + } + } +} \ No newline at end of file diff --git a/tests/Benchmarks/Modules.Benchmark/Sources/DeepCopyBenchmarkSource.cs b/tests/Benchmarks/Modules.Benchmark/Sources/DeepCopyBenchmarkSource.cs new file mode 100644 index 00000000..85dffa2f --- /dev/null +++ b/tests/Benchmarks/Modules.Benchmark/Sources/DeepCopyBenchmarkSource.cs @@ -0,0 +1,38 @@ +namespace SpaceEngineers.Core.Modules.Benchmark.Sources +{ + using System; + using Basics; + using Basics.Test.DeepCopy; + using BenchmarkDotNet.Attributes; + using BenchmarkDotNet.Engines; + + /// + /// ObjectExtensions.DeepCopy benchmark source + /// + [SimpleJob(RunStrategy.Throughput)] + public class DeepCopyBenchmarkSource + { + private TestReferenceWithoutSystemTypes? _original; + + private TestReferenceWithoutSystemTypes Original => _original ?? throw new InvalidOperationException(nameof(_original)); + + /// + /// GlobalSetup + /// + [GlobalSetup] + public void GlobalSetup() + { + _original = TestReferenceWithoutSystemTypes.CreateOrInit(); + } + + /// DeepCopyBySerialization + /// Copy + [Benchmark(Description = nameof(DeepCopyBySerialization), Baseline = true)] + public TestReferenceWithoutSystemTypes DeepCopyBySerialization() => Original.DeepCopyBySerialization(); + + /// DeepCopyByReflection + /// Copy + [Benchmark(Description = nameof(DeepCopyByReflection))] + public TestReferenceWithoutSystemTypes DeepCopyByReflection() => Original.DeepCopy(); + } +} \ No newline at end of file diff --git a/tests/Benchmarks/Modules.Benchmark/Sources/StreamCopyVersusStreamReadBenchmarkSource.cs b/tests/Benchmarks/Modules.Benchmark/Sources/StreamCopyVersusStreamReadBenchmarkSource.cs new file mode 100644 index 00000000..8e747dcf --- /dev/null +++ b/tests/Benchmarks/Modules.Benchmark/Sources/StreamCopyVersusStreamReadBenchmarkSource.cs @@ -0,0 +1,66 @@ +namespace SpaceEngineers.Core.Modules.Benchmark.Sources +{ + using System; + using System.IO; + using Basics; + using BenchmarkDotNet.Attributes; + using BenchmarkDotNet.Engines; + + /// + /// Measures Stream.CopyTo vs. Stream.Read + /// + [SimpleJob(RunStrategy.Throughput)] + public class StreamCopyVersusStreamReadBenchmarkSource + { + private FileInfo? _file; + + /// + /// GlobalSetup + /// + [GlobalSetup] + public void GlobalSetup() + { + var solutionFileDirectory = SolutionExtensions.SolutionFile().Directory + ?? throw new InvalidOperationException("Unable to find solution directory"); + + _file = solutionFileDirectory + .StepInto("tests") + .StepInto("Benchmarks") + .StepInto("Modules.Benchmark") + .StepInto("Settings") + .GetFile("appsettings", ".json"); + } + + /// CopyTo + /// Copy + [Benchmark(Description = nameof(CopyTo), Baseline = true)] + public ReadOnlyMemory CopyTo() + { + using (var stream = File.Open(_file.FullName, FileMode.Open, FileAccess.Read, FileShare.ReadWrite)) + using (var memoryStream = new MemoryStream()) + { + stream.CopyTo(memoryStream); + + return memoryStream.AsBytes(); + } + } + + /// Read + /// Copy + [Benchmark(Description = nameof(Read))] + public ReadOnlyMemory Read() + { + using (var stream = File.Open(_file.FullName, FileMode.Open, FileAccess.Read, FileShare.ReadWrite)) + { + var length = (int)stream.Length; + var buffer = new byte[length]; + + stream.Position = 0; + + _ = stream.Read(buffer); + + return buffer.AsMemory(); + } + } + } +} \ No newline at end of file diff --git a/tests/Tests/Basics.Test/AsyncSynchronizationPrimitivesTest.cs b/tests/Tests/Basics.Test/AsyncSynchronizationPrimitivesTest.cs new file mode 100644 index 00000000..f2320c4f --- /dev/null +++ b/tests/Tests/Basics.Test/AsyncSynchronizationPrimitivesTest.cs @@ -0,0 +1,184 @@ +namespace SpaceEngineers.Core.Basics.Test +{ + using System; + using System.Linq; + using System.Threading; + using System.Threading.Tasks; + using Primitives; + using Xunit; + using Xunit.Abstractions; + + /// + /// Test for async synchronization primitives + /// + public class AsyncSynchronizationPrimitivesTest : BasicsTestBase + { + private static readonly TimeSpan TestTimeout = TimeSpan.FromSeconds(1); + + /// .cctor + /// ITestOutputHelper + public AsyncSynchronizationPrimitivesTest(ITestOutputHelper output) + : base(output) + { + } + + [Fact] + internal async Task TaskCancellationCompletionSourceTest() + { + try + { + using (var cts = new CancellationTokenSource()) + using (var tcs = new TaskCancellationCompletionSource(cts.Token)) + { + cts.Cancel(); + await tcs.Task.ConfigureAwait(false); + } + } + catch (OperationCanceledException) + { + return; + } + + Assert.True(false); + } + + [Fact] + internal async Task WaitAsyncCancellationTest() + { + try + { + using (var cts = new CancellationTokenSource()) + { + var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + var task = Basics.TaskExtensions.WaitAsync(tcs.Task, cts.Token); + + cts.Cancel(); + await task.ConfigureAwait(false); + } + } + catch (OperationCanceledException) + { + return; + } + + Assert.True(false); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + internal async Task AsyncManualResetEventTest(bool isSet) + { + var manualResetEvent = new AsyncManualResetEvent(isSet); + + var waitTask1 = manualResetEvent.WaitAsync(); + var waitTask2 = manualResetEvent.WaitAsync(); + var waitTask3 = manualResetEvent.WaitAsync(); + + var waitAll = Task.WhenAll(waitTask1, waitTask2, waitTask3); + + Assert.True(isSet ? waitAll.IsCompleted : !waitAll.IsCompleted); + + if (!isSet) + { + manualResetEvent.Set(); + } + + var actual = await Task.WhenAny(waitAll, Task.Delay(TestTimeout)).ConfigureAwait(false); + + Assert.Equal(waitAll, actual); + + await actual.ConfigureAwait(false); + + Assert.True(manualResetEvent.WaitAsync().IsCompleted); + + manualResetEvent.Reset(); + + var waitTaskAfterReset1 = manualResetEvent.WaitAsync(); + var waitTaskAfterReset2 = manualResetEvent.WaitAsync(); + var waitTaskAfterReset3 = manualResetEvent.WaitAsync(); + + Assert.False(waitTaskAfterReset1.IsCompleted); + Assert.False(waitTaskAfterReset2.IsCompleted); + Assert.False(waitTaskAfterReset3.IsCompleted); + + var waitAllAfterReset = Task.WhenAll(waitTask1, waitTask2, waitTask3); + + manualResetEvent.Set(); + + var actualAfterReset = await Task.WhenAny(waitAllAfterReset, Task.Delay(TestTimeout)).ConfigureAwait(false); + + Assert.Equal(waitAllAfterReset, actualAfterReset); + + await actualAfterReset.ConfigureAwait(false); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + internal async Task AsyncAutoResetEventTest(bool isSet) + { + var autoResetEvent = new AsyncAutoResetEvent(isSet); + + var waitTask1 = autoResetEvent.WaitAsync(); + var waitTask2 = autoResetEvent.WaitAsync(); + var waitTask3 = autoResetEvent.WaitAsync(); + + Assert.True(isSet ? waitTask1.IsCompleted : !waitTask1.IsCompleted); + + var waitAll = Task.WhenAll(waitTask1, waitTask2, waitTask3); + var timeout = Task.Delay(TestTimeout); + var actual = await Task.WhenAny(waitAll, timeout).ConfigureAwait(false); + + Assert.Equal(timeout, actual); + + await actual.ConfigureAwait(false); + + var range = isSet ? 2 : 3; + Enumerable.Range(0, range).Each(_ => autoResetEvent.Set()); + + timeout = Task.Delay(TestTimeout); + actual = await Task.WhenAny(waitAll, timeout).ConfigureAwait(false); + + Assert.Equal(waitAll, actual); + + await actual.ConfigureAwait(false); + + Assert.False(autoResetEvent.WaitAsync().IsCompleted); + } + + [Theory] + [InlineData(0)] + [InlineData(3)] + internal async Task AsyncCountdownEventTest(int initialCount) + { + var countdownEvent = new AsyncCountdownEvent(initialCount); + + Assert.True(initialCount <= 0 + ? countdownEvent.WaitAsync().IsCompleted + : !countdownEvent.WaitAsync().IsCompleted); + + if (initialCount <= 0) + { + Enumerable + .Range(0, 3) + .Each(_ => countdownEvent.Increment()); + } + + Assert.Equal(3, countdownEvent.Read()); + Assert.False(countdownEvent.WaitAsync().IsCompleted); + + Enumerable + .Range(0, 3 - 1) + .Each(_ => + { + countdownEvent.Decrement(); + Assert.False(countdownEvent.WaitAsync().IsCompleted); + }); + + Assert.Equal(0, countdownEvent.Decrement()); + Assert.True(countdownEvent.WaitAsync().IsCompleted); + await countdownEvent.WaitAsync().ConfigureAwait(false); + } + } +} \ No newline at end of file diff --git a/tests/Tests/Basics.Test/AsyncUnitOfWorkTest.cs b/tests/Tests/Basics.Test/AsyncUnitOfWorkTest.cs new file mode 100644 index 00000000..90c80230 --- /dev/null +++ b/tests/Tests/Basics.Test/AsyncUnitOfWorkTest.cs @@ -0,0 +1,192 @@ +namespace SpaceEngineers.Core.Basics.Test +{ + using System; + using System.Threading; + using System.Threading.Tasks; + using Enumerations; + using Primitives; + using Xunit; + using Xunit.Abstractions; + using Xunit.Sdk; + + /// + /// AsyncUnitOfWorkTest + /// + public class AsyncUnitOfWorkTest : BasicsTestBase + { + private static readonly Func EmptyProducer = (_, _) => Task.CompletedTask; + private static readonly Func ErrorProducer = (_, _) => throw TestExtensions.TrueException(); + + /// .ctor + /// ITestOutputHelper + public AsyncUnitOfWorkTest(ITestOutputHelper output) + : base(output) + { + } + + [Fact] + internal void CommitTest() + { + var unitOfWork = new TestAsyncUnitOfWork(EnUnitOfWorkBehavior.Regular); + ExecuteInTransaction(unitOfWork, true, EmptyProducer); + + Assert.True(unitOfWork.Started); + Assert.True(unitOfWork.Committed); + Assert.False(unitOfWork.RolledBack); + Assert.False(unitOfWork.RolledBackByException); + } + + [Fact] + internal void RollbackTest() + { + var unitOfWork = new TestAsyncUnitOfWork(EnUnitOfWorkBehavior.Regular); + ExecuteInTransaction(unitOfWork, false, EmptyProducer); + + Assert.True(unitOfWork.Started); + Assert.False(unitOfWork.Committed); + Assert.True(unitOfWork.RolledBack); + Assert.False(unitOfWork.RolledBackByException); + } + + [Fact] + internal void RollbackByExceptionTest() + { + var unitOfWork = new TestAsyncUnitOfWork(EnUnitOfWorkBehavior.Regular); + Assert.Throws(() => ExecuteInTransaction(unitOfWork, true, ErrorProducer)); + + Assert.True(unitOfWork.Started); + Assert.False(unitOfWork.Committed); + Assert.True(unitOfWork.RolledBack); + Assert.True(unitOfWork.RolledBackByException); + } + + [Fact] + internal void NestedStartsTest() + { + var unitOfWork = new TestAsyncUnitOfWork(EnUnitOfWorkBehavior.Regular); + Assert.Throws(() => ExecuteInTransaction(unitOfWork, true, NestedProducer)); + + Assert.True(unitOfWork.Started); + Assert.False(unitOfWork.Committed); + Assert.True(unitOfWork.RolledBack); + Assert.True(unitOfWork.RolledBackByException); + + Task NestedProducer(object context, CancellationToken token) + { + ExecuteInTransaction(unitOfWork, true, EmptyProducer); + return Task.CompletedTask; + } + } + + [Fact] + internal void DoNotRunBehaviorTest() + { + var unitOfWork = new TestAsyncUnitOfWork(EnUnitOfWorkBehavior.DoNotRun); + ExecuteInTransaction(unitOfWork, true, EmptyProducer); + + Assert.False(unitOfWork.Started); + Assert.False(unitOfWork.Committed); + Assert.False(unitOfWork.RolledBack); + Assert.False(unitOfWork.RolledBackByException); + } + + [Fact] + internal void SkipProducerBehaviorCommitTest() + { + var unitOfWork = new TestAsyncUnitOfWork(EnUnitOfWorkBehavior.SkipProducer); + + var producerExecuted = false; + + ExecuteInTransaction(unitOfWork, true, TrackableProducer); + + Assert.True(unitOfWork.Started); + Assert.True(unitOfWork.Committed); + Assert.False(unitOfWork.RolledBack); + Assert.False(unitOfWork.RolledBackByException); + Assert.False(producerExecuted); + + Task TrackableProducer(object state, CancellationToken token) + { + producerExecuted = true; + return Task.CompletedTask; + } + } + + [Fact] + internal void SkipProducerBehaviorRollbackTest() + { + var unitOfWork = new TestAsyncUnitOfWork(EnUnitOfWorkBehavior.SkipProducer); + + var producerExecuted = false; + + ExecuteInTransaction(unitOfWork, false, TrackableProducer); + + Assert.True(unitOfWork.Started); + Assert.False(unitOfWork.Committed); + Assert.True(unitOfWork.RolledBack); + Assert.False(unitOfWork.RolledBackByException); + Assert.False(producerExecuted); + + Task TrackableProducer(object state, CancellationToken token) + { + producerExecuted = true; + return Task.CompletedTask; + } + } + + private static void ExecuteInTransaction( + IAsyncUnitOfWork unitOfWork, + bool saveChanges, + Func producer) + { + ExecutionExtensions + .Try(ExecuteInTransaction, (unitOfWork, saveChanges, producer)) + .Catch(ex => throw ex.RealException()) + .Invoke(); + } + + private static void ExecuteInTransaction( + (IAsyncUnitOfWork, bool, Func) state) + { + var (unitOfWork, saveChanges, producer) = state; + unitOfWork.ExecuteInTransaction(new object(), producer, saveChanges, CancellationToken.None).Wait(); + } + + private class TestAsyncUnitOfWork : AsyncUnitOfWork + { + private readonly EnUnitOfWorkBehavior _behavior; + + public TestAsyncUnitOfWork(EnUnitOfWorkBehavior behavior) + { + _behavior = behavior; + } + + internal bool Started { get; private set; } + + internal bool Committed { get; private set; } + + internal bool RolledBack { get; private set; } + + internal bool RolledBackByException { get; private set; } + + protected override Task Start(object context, CancellationToken token) + { + Started = _behavior != EnUnitOfWorkBehavior.DoNotRun; + return Task.FromResult(_behavior); + } + + protected override Task Commit(object context, CancellationToken token) + { + Committed = true; + return Task.CompletedTask; + } + + protected override Task Rollback(object context, Exception? exception, CancellationToken token) + { + RolledBack = true; + RolledBackByException = exception != null; + return Task.CompletedTask; + } + } + } +} \ No newline at end of file diff --git a/tests/Tests/Basics.Test/Basics.Test.csproj b/tests/Tests/Basics.Test/Basics.Test.csproj new file mode 100644 index 00000000..7849b9c6 --- /dev/null +++ b/tests/Tests/Basics.Test/Basics.Test.csproj @@ -0,0 +1,73 @@ + + + + net7.0 + SpaceEngineers.Core.Basics.Test + SpaceEngineers.Core.Basics.Test + false + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + BasicsTestBase.cs + + + BasicsTestBase.cs + + + BasicsTestBase.cs + + + BasicsTestBase.cs + + + TypeExtensionsTest.cs + + + BasicsTestBase.cs + + + BasicsTestBase.cs + + + BasicsTestBase.cs + + + BasicsTestBase.cs + + + BasicsTestBase.cs + + + BasicsTestBase.cs + + + BasicsTestBase.cs + + + BasicsTestBase.cs + + + TypeExtensionsTest.cs + + + \ No newline at end of file diff --git a/tests/Tests/Basics.Test/BasicsTestBase.cs b/tests/Tests/Basics.Test/BasicsTestBase.cs new file mode 100644 index 00000000..0e7357ef --- /dev/null +++ b/tests/Tests/Basics.Test/BasicsTestBase.cs @@ -0,0 +1,22 @@ +namespace SpaceEngineers.Core.Basics.Test +{ + using Xunit.Abstractions; + + /// + /// Unit test base class + /// + public abstract class BasicsTestBase + { + /// .ctor + /// ITestOutputHelper + protected BasicsTestBase(ITestOutputHelper output) + { + Output = output; + } + + /// + /// ITestOutputHelper + /// + protected ITestOutputHelper Output { get; } + } +} \ No newline at end of file diff --git a/tests/Tests/Basics.Test/DeepCopy/TestEnum.cs b/tests/Tests/Basics.Test/DeepCopy/TestEnum.cs new file mode 100644 index 00000000..ce46fa39 --- /dev/null +++ b/tests/Tests/Basics.Test/DeepCopy/TestEnum.cs @@ -0,0 +1,15 @@ +namespace SpaceEngineers.Core.Basics.Test.DeepCopy +{ + internal enum TestEnum + { + /// + /// Default + /// + Default, + + /// + /// Value + /// + Value + } +} \ No newline at end of file diff --git a/tests/Tests/Basics.Test/DeepCopy/TestReferenceWithSystemTypes.cs b/tests/Tests/Basics.Test/DeepCopy/TestReferenceWithSystemTypes.cs new file mode 100644 index 00000000..50e1a650 --- /dev/null +++ b/tests/Tests/Basics.Test/DeepCopy/TestReferenceWithSystemTypes.cs @@ -0,0 +1,39 @@ +namespace SpaceEngineers.Core.Basics.Test.DeepCopy +{ + using System; + using System.Collections.Generic; + + /// + /// TestReferenceWithSystemTypes + /// + [Serializable] + public class TestReferenceWithSystemTypes : TestReferenceWithoutSystemTypes + { + /* + * System.Type + */ + internal Type? Type { get; set; } + + internal Array? TypeArray { get; set; } + + internal ICollection? TypeCollection { get; set; } + + /// + /// Create TestReferenceWithSystemTypes instance + /// + /// TestReferenceWithSystemTypes instance + public static TestReferenceWithSystemTypes Create() + { + var instance = new TestReferenceWithSystemTypes + { + Type = typeof(TestReferenceWithoutSystemTypes), + TypeArray = new[] { typeof(TestReferenceWithoutSystemTypes), typeof(string), typeof(int) }, + TypeCollection = new List { typeof(TestReferenceWithoutSystemTypes), typeof(string), typeof(int) } + }; + + CreateOrInit(instance); + + return instance; + } + } +} \ No newline at end of file diff --git a/tests/Tests/Basics.Test/DeepCopy/TestReferenceWithoutSystemTypes.cs b/tests/Tests/Basics.Test/DeepCopy/TestReferenceWithoutSystemTypes.cs new file mode 100644 index 00000000..d92b3f3e --- /dev/null +++ b/tests/Tests/Basics.Test/DeepCopy/TestReferenceWithoutSystemTypes.cs @@ -0,0 +1,77 @@ +namespace SpaceEngineers.Core.Basics.Test.DeepCopy +{ + using System; + using System.Collections.Generic; + using System.Diagnostics.CodeAnalysis; + + /// + /// TestReferenceWithoutSystemTypes + /// + [SuppressMessage("Analysis", "CA5362", Justification = "For test reasons")] + [Serializable] + public class TestReferenceWithoutSystemTypes + { + /* + * String + */ + internal string? String { get; set; } + + /* + * ValueType + */ + internal int Int { get; set; } + + internal TestEnum TestEnum { get; set; } + + internal Array? ValueTypeArray { get; set; } + + internal ICollection? ValueTypeCollection { get; set; } + + /* + * ReferenceType + */ + internal Array? ReferenceTypeArray { get; set; } + + internal ICollection? ReferenceTypeCollection { get; set; } + + internal TestReferenceWithoutSystemTypes? CyclicReference { get; set; } + + internal static TestReferenceWithoutSystemTypes? StaticCyclicReference { get; set; } + + /* + * Nullable + */ + internal int? NullableInt { get; } + + internal TestReferenceWithoutSystemTypes? NullableReference { get; } + + internal Array? ArrayOfNulls { get; set; } + + internal ICollection? CollectionOfNulls { get; set; } + + /// + /// CreateOrInit TestReferenceWithoutSystemTypes instance + /// + /// TestReferenceWithoutSystemTypes (optional) + /// Initialized TestReferenceWithoutSystemTypes instance + public static TestReferenceWithoutSystemTypes CreateOrInit(TestReferenceWithoutSystemTypes? instance = null) + { + instance ??= new TestReferenceWithoutSystemTypes(); + + instance.String = "PublicString123#'!"; + instance.Int = 100; + instance.TestEnum = TestEnum.Value; + instance.ValueTypeArray = new[] { 1, 2, 3, 4, 5 }; + instance.ValueTypeCollection = new List { 1, 2, 3, 4, 5 }; + instance.ReferenceTypeArray = new[] { new object(), new object(), new object() }; + instance.ReferenceTypeCollection = new[] { new object(), new object(), new object() }; + instance.ArrayOfNulls = new object?[] { null, null, null }; + instance.CollectionOfNulls = new List { null, null, null }; + + instance.CyclicReference = instance; + StaticCyclicReference = instance; + + return instance; + } + } +} \ No newline at end of file diff --git a/tests/Tests/Basics.Test/DeferredQueueTest.cs b/tests/Tests/Basics.Test/DeferredQueueTest.cs new file mode 100644 index 00000000..7921c6cc --- /dev/null +++ b/tests/Tests/Basics.Test/DeferredQueueTest.cs @@ -0,0 +1,269 @@ +namespace SpaceEngineers.Core.Basics.Test +{ + using System; + using System.Collections.Generic; + using System.Linq; + using System.Threading; + using System.Threading.Tasks; + using Enumerations; + using Primitives; + using Xunit; + using Xunit.Abstractions; + + /// + /// DeferredQueue primitive test + /// + public class DeferredQueueTest : BasicsTestBase + { + /// .cctor + /// ITestOutputHelper + public DeferredQueueTest(ITestOutputHelper output) + : base(output) + { + } + + /// + /// OnRootNodeChangedTestData + /// + /// TestData + public static IEnumerable DeferredQueueTestData() + { + var emptyQueue = new DeferredQueue(new BinaryHeap>(EnOrderingDirection.Asc), PrioritySelector); + + yield return new object[] { emptyQueue }; + + static DateTime PrioritySelector(Entry entry) => entry.Planned; + } + + [Theory(Timeout = 60_000)] + [MemberData(nameof(DeferredQueueTestData))] + internal async Task OnRootNodeChangedTest(DeferredQueue queue) + { + Assert.Throws(queue.Dequeue); + Assert.Throws(() => queue.TryDequeue(out _)); + Assert.Throws(queue.Peek); + Assert.Throws(() => queue.TryPeek(out _)); + + Assert.True(queue.IsEmpty); + + var step = TimeSpan.FromMilliseconds(100); + var startFrom = DateTime.UtcNow.Add(step); + + queue.Enqueue(new Entry(0, startFrom)); + queue.Enqueue(new Entry(2, startFrom.Add(2 * step))); + queue.Enqueue(new Entry(4, startFrom.Add(4 * step))); + + var entries = new List(); + var started = DateTime.UtcNow; + Output.WriteLine($"Started at: {started:O}"); + using (var cts = new CancellationTokenSource(TimeSpan.FromSeconds(3))) + { + var backgroundPublisher = Task.Run(async () => + { + var corrected = startFrom.Add(step / 2) - DateTime.UtcNow; + + await Task.Delay(corrected, cts.Token).ConfigureAwait(false); + queue.Enqueue(new Entry(1, startFrom.Add(1 * step))); + + await Task.Delay(step, cts.Token).ConfigureAwait(false); + queue.Enqueue(new Entry(3, startFrom.Add(3 * step))); + + await Task.Delay(step, cts.Token).ConfigureAwait(false); + queue.Enqueue(new Entry(5, startFrom.Add(5 * step))); + }, + cts.Token); + + var deferredDeliveryOperation = queue.Run(Callback, cts.Token); + + await backgroundPublisher.ConfigureAwait(false); + + try + { + await deferredDeliveryOperation.ConfigureAwait(false); + } + catch (OperationCanceledException) + { + } + } + + entries.Each(entry => Output.WriteLine(entry.ToString())); + Assert.True(queue.IsEmpty); + Assert.True(started <= entries.First().Planned); + + var deltas = new List(); + _ = entries.Aggregate((prev, next) => + { + var delta = next.Actual - prev.Actual; + deltas.Add(delta); + return next; + }); + + deltas.Each(delta => Output.WriteLine(delta.ToString())); + Assert.Equal(Enumerable.Range(0, 6).ToArray(), entries.Select(entry => entry.Index).ToArray()); + + Task Callback(Entry entry, CancellationToken token) + { + entry.Actual = DateTime.UtcNow; + entries.Add(entry); + + return Task.CompletedTask; + } + } + + [Theory(Timeout = 60_000)] + [MemberData(nameof(DeferredQueueTestData))] + internal async Task IntensiveReadWriteTest(DeferredQueue queue) + { + Assert.True(queue.IsEmpty); + + var publishersCount = 10; + var publicationsCount = 100; + + var step = TimeSpan.FromMilliseconds(10); + var startFrom = DateTime.UtcNow.Add(TimeSpan.FromMilliseconds(100)); + + var actualCount = 0; + var expectedCount = publishersCount * publicationsCount; + + var started = DateTime.UtcNow; + + using (var cts = new CancellationTokenSource(TimeSpan.FromSeconds(3))) + { + var token = cts.Token; + + var deferredDeliveryOperation = queue.Run(Callback(cts), token); + + var publishers = Enumerable.Range(0, publishersCount) + .Select(i => Task.Run(() => StartPublishing(queue, publicationsCount, startFrom.Add(step * i), step, token), token)) + .ToList(); + + try + { + await Task.WhenAll(publishers).ConfigureAwait(false); + await deferredDeliveryOperation.ConfigureAwait(false); + } + catch (OperationCanceledException) + { + } + } + + var finished = DateTime.UtcNow; + var duration = finished - started; + Output.WriteLine(duration.ToString()); + + Assert.True(queue.IsEmpty); + Assert.Equal(expectedCount, actualCount); + + Func Callback(CancellationTokenSource cts) + { + return (_, _) => + { + Interlocked.Increment(ref actualCount); + + if (expectedCount == actualCount) + { + cts.Cancel(); + } + + return Task.CompletedTask; + }; + } + + static async Task StartPublishing( + IAsyncQueue queue, + int publicationsCount, + DateTime startFrom, + TimeSpan step, + CancellationToken token) + { + for (var i = 1; i <= publicationsCount; i++) + { + await queue.Enqueue(new Entry(i, startFrom.Add(step * i)), token).ConfigureAwait(false); + } + } + } + + internal class Entry : IEquatable, + ISafelyEquatable, + ISafelyComparable, + IComparable, + IComparable + { + private DateTime? _actual; + + public Entry(int index, DateTime planned) + { + Index = index; + Planned = planned; + } + + public int Index { get; } + + public DateTime Planned { get; } + + public DateTime Actual + { + get => _actual ?? throw new InvalidOperationException("Elapsed should be set"); + set => _actual = value; + } + + #region IEquatable + + public static bool operator ==(Entry? left, Entry? right) + { + return Equatable.Equals(left, right); + } + + public static bool operator !=(Entry? left, Entry? right) + { + return !Equatable.Equals(left, right); + } + + public override int GetHashCode() + { + return Index; + } + + public override bool Equals(object? obj) + { + return Equatable.Equals(this, obj); + } + + public bool Equals(Entry? other) + { + return Equatable.Equals(this, other); + } + + public bool SafeEquals(Entry other) + { + return Index == other.Index; + } + + #endregion + + #region IComparable + + public int SafeCompareTo(Entry other) + { + return Index.CompareTo(other.Index); + } + + public int CompareTo(Entry? other) + { + return Comparable.CompareTo(this, other); + } + + public int CompareTo(object? obj) + { + return Comparable.CompareTo(this, obj); + } + + #endregion + + public override string ToString() + { + return $"[{Index}] - {Planned:O} - {_actual?.ToString("O") ?? "null"}"; + } + } + } +} \ No newline at end of file diff --git a/tests/Tests/Basics.Test/EnumerableExtensionsTest.cs b/tests/Tests/Basics.Test/EnumerableExtensionsTest.cs new file mode 100644 index 00000000..ec09f959 --- /dev/null +++ b/tests/Tests/Basics.Test/EnumerableExtensionsTest.cs @@ -0,0 +1,222 @@ +namespace SpaceEngineers.Core.Basics.Test +{ + using System; + using System.Collections.Generic; + using System.Linq; + using Xunit; + using Xunit.Abstractions; + + /// + /// EnumerableExtensions test + /// + public class EnumerableExtensionsTest : BasicsTestBase + { + /// .ctor + /// ITestOutputHelper + public EnumerableExtensionsTest(ITestOutputHelper output) + : base(output) + { + } + + [Fact] + internal void SimpleColumnsCartesianProductTest() + { + IEnumerable> columns = new List> + { + new List + { + typeof(object), + typeof(bool), + typeof(string), + typeof(Enum) + } + }; + + IEnumerable> expected = new List> + { + new List + { + typeof(object) + }, + new List + { + typeof(bool) + }, + new List + { + typeof(string) + }, + new List + { + typeof(Enum) + } + }; + + Assert.True(CheckEquality(expected, columns.ColumnsCartesianProduct())); + + columns = new List> + { + new List + { + typeof(object), + typeof(bool) + }, + new List + { + typeof(string), + typeof(Enum) + } + }; + + expected = new List> + { + new List + { + typeof(object), + typeof(string) + }, + new List + { + typeof(object), + typeof(Enum) + }, + new List + { + typeof(bool), + typeof(string) + }, + new List + { + typeof(bool), + typeof(Enum) + } + }; + + Assert.True(CheckEquality(expected, columns.ColumnsCartesianProduct())); + } + + [Fact] + internal void ComplexColumnsCartesianProductTest() + { + IEnumerable> columns = new List> + { + new List + { + typeof(object), + typeof(bool) + }, + new List + { + typeof(string), + typeof(Enum) + }, + new List + { + typeof(int), + typeof(decimal) + } + }; + + IEnumerable> expected = new List> + { + new List + { + typeof(object), + typeof(string), + typeof(int) + }, + new List + { + typeof(object), + typeof(string), + typeof(decimal) + }, + new List + { + typeof(object), + typeof(Enum), + typeof(int) + }, + new List + { + typeof(object), + typeof(Enum), + typeof(decimal) + }, + new List + { + typeof(bool), + typeof(string), + typeof(int) + }, + new List + { + typeof(bool), + typeof(string), + typeof(decimal) + }, + new List + { + typeof(bool), + typeof(Enum), + typeof(int) + }, + new List + { + typeof(bool), + typeof(Enum), + typeof(decimal) + } + }; + + Assert.True(CheckEquality(expected, columns.ColumnsCartesianProduct())); + } + + [Fact] + internal void EmptyColumnsCartesianProductTest() + { + IEnumerable> columns = Enumerable.Empty>(); + IEnumerable> expected = Enumerable.Empty>(); + + Assert.True(CheckEquality(expected, columns.ColumnsCartesianProduct())); + + columns = new List> + { + new List + { + typeof(object), + typeof(bool) + }, + new List + { + typeof(string), + typeof(Enum) + }, + Enumerable.Empty() + }; + + Assert.True(CheckEquality(expected, columns.ColumnsCartesianProduct())); + } + + private bool CheckEquality(IEnumerable> expected, IEnumerable> actual) + { + Show(actual); + + return expected.Count() == actual.Count() + && expected.Zip(actual).All(pair => pair.First.SequenceEqual(pair.Second)); + } + + private void Show(IEnumerable> source) + { + foreach (var row in source) + { + foreach (var item in row) + { + Output.WriteLine(item.Name); + } + + Output.WriteLine("-*-*-*-*-*-"); + } + } + } +} \ No newline at end of file diff --git a/tests/Tests/Basics.Test/ExecutionExtensionsActionsTest.cs b/tests/Tests/Basics.Test/ExecutionExtensionsActionsTest.cs new file mode 100644 index 00000000..41e5265a --- /dev/null +++ b/tests/Tests/Basics.Test/ExecutionExtensionsActionsTest.cs @@ -0,0 +1,113 @@ +namespace SpaceEngineers.Core.Basics.Test +{ + using System; + using Basics; + using Xunit; + using Xunit.Abstractions; + using Xunit.Sdk; + + /// + /// ExecutionExtensions class tests + /// + public class ExecutionExtensionsActionsTest : BasicsTestBase + { + /// .ctor + /// ITestOutputHelper + public ExecutionExtensionsActionsTest(ITestOutputHelper output) + : base(output) + { + } + + [Fact] + internal void HandleCaughtExceptionsTest() + { + Action action = () => throw TestExtensions.TrueException(); + + ExecutionExtensions.Try(action).Catch().Invoke(); + ExecutionExtensions.Try(action).Catch(ex => { }).Invoke(); + + void HandleCaught() => ExecutionExtensions + .Try(action) + .Catch(ex => throw ex) + .Invoke(); + + Assert.Throws(HandleCaught); + + void Rethrow() => ExecutionExtensions + .Try(action) + .Catch(ex => throw TestExtensions.FalseException()) + .Invoke(); + + Assert.Throws(Rethrow); + + void Unhandled() => ExecutionExtensions + .Try(action) + .Catch(_ => throw TestExtensions.FalseException()) + .Invoke(); + + Assert.Throws(Unhandled); + } + + [Fact] + internal void SimpleTest() + { + Action action = () => { }; + + ExecutionExtensions + .Try(action) + .Catch() + .Catch() + .Invoke(); + } + + [Fact] + internal void HandledExceptionTest() + { + Action action = () => throw TestExtensions.FalseException(); + + ExecutionExtensions + .Try(action) + .Catch() + .Invoke(); + } + + [Fact] + internal void SeveralCatchBlocksTest() + { + Action action = () => throw TestExtensions.FalseException(); + + ExecutionExtensions + .Try(action) + .Catch(ex => throw ex) + .Catch() + .Invoke(); + } + + [Fact] + internal void ThrowInCatchBlockTest() + { + Action action = () => throw TestExtensions.FalseException(); + + void TestAction() => ExecutionExtensions + .Try(action) + .Catch(ex => throw TestExtensions.TrueException()) + .Invoke(); + + Assert.Throws(TestAction); + } + + [Fact] + internal void ThrowInFinallyBlockTest() + { + Action action = () => throw TestExtensions.FalseException(); + + void TestAction() => ExecutionExtensions + .Try(action) + .Catch(ex => throw ex) + .Finally(() => throw TestExtensions.TrueException()) + .Invoke(); + + Assert.Throws(TestAction); + } + } +} \ No newline at end of file diff --git a/tests/Tests/Basics.Test/ExecutionExtensionsFunctionsTest.cs b/tests/Tests/Basics.Test/ExecutionExtensionsFunctionsTest.cs new file mode 100644 index 00000000..58123bba --- /dev/null +++ b/tests/Tests/Basics.Test/ExecutionExtensionsFunctionsTest.cs @@ -0,0 +1,142 @@ +namespace SpaceEngineers.Core.Basics.Test +{ + using System; + using Basics; + using Xunit; + using Xunit.Abstractions; + using Xunit.Sdk; + + /// + /// ExecutionExtensions class tests + /// + public class ExecutionExtensionsFunctionsTest : BasicsTestBase + { + /// .ctor + /// ITestOutputHelper + public ExecutionExtensionsFunctionsTest(ITestOutputHelper output) + : base(output) + { + } + + [Fact] + internal void HandleCaughtExceptionsTest() + { + Func func = () => throw TestExtensions.TrueException(); + + var emptyHandlerBlockResult = ExecutionExtensions + .Try(func) + .Catch() + .Invoke(_ => default); + + Assert.False(emptyHandlerBlockResult); + + emptyHandlerBlockResult = ExecutionExtensions + .Try(func) + .Catch(_ => { }) + .Invoke(_ => default); + + Assert.False(emptyHandlerBlockResult); + + var result = ExecutionExtensions + .Try(func) + .Catch() + .Invoke(_ => true); + + Assert.True(result); + + void HandleNotCaught() => ExecutionExtensions + .Try(func) + .Catch() + .Invoke(_ => true); + + Assert.Throws(HandleNotCaught); + } + + [Fact] + internal void SimpleTest() + { + // 1 - value type + Func valueTypeFunction = () => true; + var result1 = ExecutionExtensions.Try(valueTypeFunction).Invoke(_ => default); + Assert.True(result1); + + // 2 - nullable value type + Func nullableValueTypeFunction = () => null; + var result2 = ExecutionExtensions.Try(nullableValueTypeFunction).Invoke(_ => default); + Assert.Null(result2); + + // 3 - reference type + Func referenceFunction = () => new object(); + var result3 = ExecutionExtensions.Try(referenceFunction).Invoke(_ => new object()); + Assert.NotNull(result3); + + // nullable-reference type + Func nullableReferenceFunction = () => null; + var result4 = ExecutionExtensions.Try(nullableReferenceFunction).Invoke(_ => default); + Assert.Null(result4); + } + + [Fact] + internal void HandledExceptionTest() + { + Func function = () => throw TestExtensions.FalseException(); + + ExecutionExtensions + .Try(function) + .Catch() + .Invoke(_ => default); + } + + [Fact] + internal void SeveralCatchBlocksTest() + { + Func function = () => throw TestExtensions.FalseException(); + + ExecutionExtensions + .Try(function) + .Catch(ex => throw ex) + .Catch() + .Invoke(_ => default); + } + + [Fact] + internal void ThrowInCatchBlockTest() + { + Func function = () => throw TestExtensions.FalseException(); + + void TestFunction() => ExecutionExtensions + .Try(function) + .Catch(_ => throw TestExtensions.TrueException()) + .Invoke(_ => default); + + Assert.Throws(TestFunction); + } + + [Fact] + internal void ThrowInInvokeBlockTest() + { + Func function = () => throw TestExtensions.FalseException(); + + void TestFunction() => ExecutionExtensions + .Try(function) + .Catch() + .Invoke(ex => throw TestExtensions.TrueException()); + + Assert.Throws(TestFunction); + } + + [Fact] + internal void ThrowInFinallyBlockTest() + { + Func function = () => throw TestExtensions.FalseException(); + + void TestFunction() => ExecutionExtensions + .Try(function) + .Catch(ex => throw ex) + .Finally(() => throw TestExtensions.TrueException()) + .Invoke(_ => default); + + Assert.Throws(TestFunction); + } + } +} \ No newline at end of file diff --git a/tests/Tests/Basics.Test/HeapTest.cs b/tests/Tests/Basics.Test/HeapTest.cs new file mode 100644 index 00000000..6fee14fe --- /dev/null +++ b/tests/Tests/Basics.Test/HeapTest.cs @@ -0,0 +1,120 @@ +namespace SpaceEngineers.Core.Basics.Test +{ + using System; + using System.Collections.Generic; + using System.Diagnostics.CodeAnalysis; + using System.Linq; + using System.Threading.Tasks; + using Enumerations; + using Primitives; + using Xunit; + using Xunit.Abstractions; + + /// + /// HeapTest + /// + public class HeapTest : BasicsTestBase + { + /// .cctor + /// ITestOutputHelper + public HeapTest(ITestOutputHelper output) + : base(output) + { + } + + /// Heap test data member + /// Test data + [SuppressMessage("Analysis", "CA5394", Justification = "Test data generation")] + public static IEnumerable HeapTestData() + { + var count = 100; + var array = new int[count]; + var random = new Random(100); + + for (var i = 0; i < count; i++) + { + array[i] = random.Next(0, count); + } + + yield return new object[] { new BinaryHeap(array, EnOrderingDirection.Asc), EnOrderingDirection.Asc }; + yield return new object[] { new BinaryHeap(array, EnOrderingDirection.Desc), EnOrderingDirection.Desc }; + } + + [Theory] + [MemberData(nameof(HeapTestData))] + internal void OrderingTest(IHeap heap, EnOrderingDirection orderingKind) + { + Output.WriteLine(heap.ToString()); + + var enumeratedArray = (orderingKind == EnOrderingDirection.Asc + ? heap.OrderBy(it => it) + : heap.OrderByDescending(it => it)) + .ToArray(); + + var orderedArray = heap.ExtractArray(); + + Assert.Equal(enumeratedArray, orderedArray); + Assert.True(heap.IsEmpty); + Assert.Equal(0, heap.Count); + } + + [Theory] + [MemberData(nameof(HeapTestData))] + internal void MultiThreadAccessTest(IHeap heap, EnOrderingDirection orderingKind) + { + var count = heap.Count; + + Output.WriteLine(heap.ToString()); + + var enumeratedArray = (orderingKind == EnOrderingDirection.Asc + ? heap.OrderBy(it => it) + : heap.OrderByDescending(it => it)) + .ToArray(); + + Parallel.For( + 0, + count, + new ParallelOptions { MaxDegreeOfParallelism = Environment.ProcessorCount }, + Modify); + + var orderedArray = heap.ExtractArray(); + + Assert.Equal(enumeratedArray, orderedArray); + Assert.True(heap.IsEmpty); + Assert.Equal(0, heap.Count); + + void Modify(int index) + { + switch (index % 2) + { + case 0: Read(); break; + default: Write(); break; + } + } + + void Read() + { + lock (heap) + { + _ = heap.Count; + _ = heap.IsEmpty; + _ = heap.Peek(); + _ = heap.TryPeek(out _); + } + } + + void Write() + { + lock (heap) + { + heap.Insert(heap.Extract()); + + if (heap.TryExtract(out var element)) + { + heap.Insert(element); + } + } + } + } + } +} \ No newline at end of file diff --git a/tests/Tests/Basics.Test/MethodExtensionsTest.cs b/tests/Tests/Basics.Test/MethodExtensionsTest.cs new file mode 100644 index 00000000..f1120c75 --- /dev/null +++ b/tests/Tests/Basics.Test/MethodExtensionsTest.cs @@ -0,0 +1,284 @@ +namespace SpaceEngineers.Core.Basics.Test +{ + using System.Diagnostics.CodeAnalysis; + using System.Linq; + using System.Reflection; + using Basics; + using Exceptions; + using Xunit; + using Xunit.Abstractions; + using Xunit.Sdk; + + /// + /// MethodExtensions class tests + /// + public class MethodExtensionsTest : BasicsTestBase + { + /// .ctor + /// ITestOutputHelper + public MethodExtensionsTest(ITestOutputHelper output) + : base(output) { } + + [Fact] + internal void CallStaticMethodTest() + { + Assert.True(typeof(StaticTestClass).CallMethod(nameof(StaticTestClass.PublicStaticMethod)).Invoke()); + Assert.True(typeof(StaticTestClass).CallMethod("PrivateStaticMethod").Invoke()); + + Assert.True(typeof(StaticTestClass).CallMethod(nameof(StaticTestClass.PublicStaticMethodWithArgs)).WithArgument(true).Invoke()); + Assert.True(typeof(StaticTestClass).CallMethod("PrivateStaticMethodWithArgs").WithArgument(true).Invoke()); + + Assert.True(typeof(StaticTestClass).CallMethod(nameof(StaticTestClass.PublicStaticMethodWithSeveralArgs)).WithArgument(true).WithArgument(true).Invoke()); + Assert.True(typeof(StaticTestClass).CallMethod(nameof(StaticTestClass.PublicStaticMethodWithSeveralArgs)).WithArgument(true).WithArgument(true).WithArgument(true).Invoke()); + Assert.True(typeof(StaticTestClass).CallMethod("PrivateStaticMethodWithSeveralArgs").WithArgument(true).WithArgument(true).Invoke()); + + Assert.Throws(() => typeof(StaticTestClass).CallMethod(nameof(StaticTestClass.PublicStaticMethodWithParams)).WithArgument(true).WithArgument(true).WithArgument(true).Invoke()); + Assert.Throws(() => typeof(StaticTestClass).CallMethod("PrivateStaticMethodWithParams").WithArgument(true).WithArgument(true).WithArgument(true).Invoke()); + + Assert.True(typeof(StaticTestClass).CallMethod(nameof(StaticTestClass.PublicStaticMethodWithParams)).WithArgument(new object[] { true, true, true }).Invoke()); + Assert.True(typeof(StaticTestClass).CallMethod("PrivateStaticMethodWithParams").WithArgument(new object[] { true, true, true }).Invoke()); + } + + [Fact] + internal void CallInstanceMethodTest() + { + var target = new InstanceTestClass(); + + Assert.True(target.CallMethod(nameof(InstanceTestClass.PublicMethod)).Invoke()); + Assert.True(target.CallMethod("PrivateMethod").Invoke()); + + Assert.True(target.CallMethod(nameof(InstanceTestClass.PublicMethodWithArgs)).WithArgument(true).Invoke()); + Assert.True(target.CallMethod("PrivateMethodWithArgs").WithArgument(true).Invoke()); + + Assert.True(target.CallMethod(nameof(InstanceTestClass.PublicMethodWithSeveralArgs)).WithArgument(true).WithArgument(true).Invoke()); + Assert.True(target.CallMethod(nameof(InstanceTestClass.PublicMethodWithSeveralArgs)).WithArgument(true).WithArgument(true).WithArgument(true).Invoke()); + Assert.True(target.CallMethod("PrivateMethodWithSeveralArgs").WithArgument(true).WithArgument(true).Invoke()); + + Assert.Throws(() => target.CallMethod(nameof(InstanceTestClass.PublicMethodWithParams)).WithArgument(true).WithArgument(true).WithArgument(true).Invoke()); + Assert.Throws(() => target.CallMethod("PrivateMethodWithParams").WithArgument(true).WithArgument(true).WithArgument(true).Invoke()); + + Assert.True(target.CallMethod(nameof(InstanceTestClass.PublicMethodWithParams)).WithArgument(new object[] { true, true, true }).Invoke()); + Assert.True(target.CallMethod("PrivateMethodWithParams").WithArgument(new object[] { true, true, true }).Invoke()); + } + + [Fact] + internal void CallStaticGenericMethodTest() + { + // 1 - close on reference + Assert.Throws(() => typeof(StaticTestClass).CallMethod(nameof(StaticTestClass.PublicStaticGenericMethod)).WithTypeArgument().Invoke()); + Assert.Throws(() => typeof(StaticTestClass).CallMethod("PrivateStaticGenericMethod").WithTypeArgument().Invoke()); + + Assert.Throws(() => typeof(StaticTestClass).CallMethod(nameof(StaticTestClass.PublicStaticGenericMethod)).WithTypeArgument().WithTypeArgument().Invoke()); + Assert.Throws(() => typeof(StaticTestClass).CallMethod("PrivateStaticGenericMethod").WithTypeArgument().WithTypeArgument().Invoke()); + + Assert.NotNull(typeof(StaticTestClass).CallMethod(nameof(StaticTestClass.PublicStaticGenericMethod)).WithTypeArgument().WithArgument(new object()).Invoke()); + Assert.NotNull(typeof(StaticTestClass).CallMethod("PrivateStaticGenericMethod").WithTypeArgument().WithArgument(new object()).Invoke()); + + Assert.NotNull(typeof(StaticTestClass).CallMethod(nameof(StaticTestClass.AmbiguousPublicStaticGenericMethod)).WithTypeArgument().WithArgument(new object()).Invoke()); + Assert.NotNull(typeof(StaticTestClass).CallMethod("AmbiguousPrivateStaticGenericMethod").WithTypeArgument().WithArgument(new object()).Invoke()); + + // 2 - close on value + Assert.True(typeof(StaticTestClass).CallMethod(nameof(StaticTestClass.PublicStaticGenericMethod)).WithTypeArgument().WithArgument(true).Invoke()); + Assert.True(typeof(StaticTestClass).CallMethod("PrivateStaticGenericMethod").WithTypeArgument().WithArgument(true).Invoke()); + + Assert.True(typeof(StaticTestClass).CallMethod(nameof(StaticTestClass.PublicStaticGenericMethod)).WithTypeArgument(typeof(bool)).WithArgument(true).Invoke()); + Assert.True(typeof(StaticTestClass).CallMethod("PrivateStaticGenericMethod").WithTypeArgument(typeof(bool)).WithArgument(true).Invoke()); + + Assert.True(typeof(StaticTestClass).CallMethod(nameof(StaticTestClass.PublicStaticGenericMethod)).WithTypeArgument().WithArgument(true).Invoke()); + Assert.True(typeof(StaticTestClass).CallMethod("PrivateStaticGenericMethod").WithTypeArgument().WithArgument(true).Invoke()); + + Assert.Throws(() => typeof(StaticTestClass).CallMethod(nameof(StaticTestClass.AmbiguousPublicStaticGenericMethod)).WithTypeArgument().WithArgument(true).Invoke()); + Assert.Throws(() => typeof(StaticTestClass).CallMethod("AmbiguousPrivateStaticGenericMethod").WithTypeArgument().WithArgument(true).Invoke()); + } + + [Fact] + internal void CallInstanceGenericMethodTest() + { + var target = new InstanceTestClass(); + + // 1 - close on reference + Assert.Throws(() => target.CallMethod(nameof(InstanceTestClass.PublicGenericMethod)).WithTypeArgument().Invoke()); + Assert.Throws(() => target.CallMethod("PrivateGenericMethod").WithTypeArgument().Invoke()); + + Assert.Throws(() => target.CallMethod(nameof(InstanceTestClass.PublicGenericMethod)).WithTypeArgument().WithTypeArgument().Invoke()); + Assert.Throws(() => target.CallMethod("PrivateGenericMethod").WithTypeArgument().WithTypeArgument().Invoke()); + + Assert.NotNull(target.CallMethod(nameof(InstanceTestClass.PublicGenericMethod)).WithTypeArgument().WithArgument(new object()).Invoke()); + Assert.NotNull(target.CallMethod("PrivateGenericMethod").WithTypeArgument().WithArgument(new object()).Invoke()); + + Assert.NotNull(target.CallMethod(nameof(InstanceTestClass.AmbiguousPublicGenericMethod)).WithTypeArgument().WithArgument(new object()).Invoke()); + Assert.NotNull(target.CallMethod("AmbiguousPrivateGenericMethod").WithTypeArgument().WithArgument(new object()).Invoke()); + + // 2 - close on value + Assert.True(target.CallMethod(nameof(InstanceTestClass.PublicGenericMethod)).WithTypeArgument().WithArgument(true).Invoke()); + Assert.True(target.CallMethod("PrivateGenericMethod").WithTypeArgument().WithArgument(true).Invoke()); + + Assert.True(target.CallMethod(nameof(InstanceTestClass.PublicGenericMethod)).WithTypeArgument(typeof(bool)).WithArgument(true).Invoke()); + Assert.True(target.CallMethod("PrivateGenericMethod").WithTypeArgument(typeof(bool)).WithArgument(true).Invoke()); + + Assert.True(target.CallMethod(nameof(InstanceTestClass.PublicGenericMethod)).WithTypeArgument().WithArgument(true).Invoke()); + Assert.True(target.CallMethod("PrivateGenericMethod").WithTypeArgument().WithArgument(true).Invoke()); + + Assert.Throws(() => target.CallMethod(nameof(InstanceTestClass.AmbiguousPublicGenericMethod)).WithTypeArgument().WithArgument(true).Invoke()); + Assert.Throws(() => target.CallMethod("AmbiguousPrivateGenericMethod").WithTypeArgument().WithArgument(true).Invoke()); + } + + [Fact] + internal void NullArgumentTest() + { + var target = new NullableTestClass(); + + Assert.Throws(() => target.CallMethod(nameof(NullableTestClass.MethodWithNullArgs)).WithArgument(null).Invoke()); + Assert.Throws(() => target.CallMethod(nameof(NullableTestClass.MethodWithOptionalNullArgs)).WithArgument(null).Invoke()); + } + + [Fact] + internal void InheritanceCallTest() + { + var baseTarget = new TestClassBase(); + var derivedTarget = new DerivedClass(); + + Assert.Throws(() => baseTarget.CallMethod(nameof(TestClassBase.BaseMethod)).Invoke()); + Assert.Throws(() => baseTarget.CallMethod("VirtualMethod").Invoke()); + Assert.Throws(() => derivedTarget.CallMethod(nameof(DerivedClass.BaseMethod)).Invoke()); + + Assert.Throws(() => derivedTarget.CallMethod(nameof(DerivedClass.DerivedMethod)).Invoke()); + Assert.Throws(() => derivedTarget.CallMethod("VirtualMethod").Invoke()); + } + + [SuppressMessage("Analysis", "SA1202", Justification = "For test reasons")] + [SuppressMessage("Analysis", "IDE0051", Justification = "For test reasons")] + private class StaticTestClass + { + internal static bool PublicStaticMethod() => true; + + private static bool PrivateStaticMethod() => true; + + internal static bool PublicStaticMethodWithArgs(bool flag) => flag; + + private static bool PrivateStaticMethodWithArgs(bool flag) => flag; + + internal static bool PublicStaticMethodWithSeveralArgs(bool flag1, bool flag2) => flag1 && flag2; + + internal static bool PublicStaticMethodWithSeveralArgs(bool flag1, bool flag2, bool flag3) => flag1 && flag2 && flag3; + + private static bool PrivateStaticMethodWithSeveralArgs(bool flag1, bool flag2) => flag1 && flag2; + + internal static bool PublicStaticMethodWithParams(params object[] flags) => flags.OfType().All(z => z); + + private static bool PrivateStaticMethodWithParams(params object[] flags) => flags.OfType().All(z => z); + + public static T PublicStaticGenericMethod() => throw TestExtensions.TrueException(); + + private static T PrivateStaticGenericMethod() => throw TestExtensions.TrueException(); + + public static T1 PublicStaticGenericMethod() => throw TestExtensions.FalseException(); + + private static T1 PrivateStaticGenericMethod() => throw TestExtensions.FalseException(); + + public static bool AmbiguousPublicStaticGenericMethod(bool flag) => flag; + + private static bool AmbiguousPrivateStaticGenericMethod(bool flag) => flag; + + public static T AmbiguousPublicStaticGenericMethod(T flag) => flag; + + private static T AmbiguousPrivateStaticGenericMethod(T flag) => flag; + + public static T PublicStaticGenericMethod(T flag) => flag; + + private static T PrivateStaticGenericMethod(T flag) => flag; + } + + [SuppressMessage("Analysis", "SA1202", Justification = "For test reasons")] + [SuppressMessage("Analysis", "IDE0051", Justification = "For test reasons")] + [SuppressMessage("Analysis", "CA1822", Justification = "For test reasons")] + private class InstanceTestClass + { + internal bool PublicMethod() => true; + + private bool PrivateMethod() => true; + + internal bool PublicMethodWithArgs(bool flag) => flag; + + private bool PrivateMethodWithArgs(bool flag) => flag; + + internal bool PublicMethodWithSeveralArgs(bool flag1, bool flag2) => flag1 && flag2; + + internal bool PublicMethodWithSeveralArgs(bool flag1, bool flag2, bool flag3) => flag1 && flag2 && flag3; + + private bool PrivateMethodWithSeveralArgs(bool flag1, bool flag2) => flag1 && flag2; + + internal bool PublicMethodWithParams(params object[] flags) => flags.OfType().All(z => z); + + private bool PrivateMethodWithParams(params object[] flags) => flags.OfType().All(z => z); + + public T PublicGenericMethod() => throw TestExtensions.TrueException(); + + private T PrivateGenericMethod() => throw TestExtensions.TrueException(); + + public T1 PublicGenericMethod() => throw TestExtensions.FalseException(); + + private T1 PrivateGenericMethod() => throw TestExtensions.FalseException(); + + public bool AmbiguousPublicGenericMethod(bool flag) => flag; + + private bool AmbiguousPrivateGenericMethod(bool flag) => flag; + + public T AmbiguousPublicGenericMethod(T input) => input; + + private T AmbiguousPrivateGenericMethod(T input) => input; + + public T PublicGenericMethod(T input) => input; + + private T PrivateGenericMethod(T input) => input; + } + + [SuppressMessage("Analysis", "CA1822", Justification = "For test reasons")] + private class NullableTestClass + { + public void MethodWithNullArgs(object? arg) + { + if (arg == null) + { + throw TestExtensions.TrueException(); + } + } + + public void MethodWithOptionalNullArgs(object? arg = null) + { + if (arg == null) + { + throw TestExtensions.TrueException(); + } + } + } + + [SuppressMessage("Analysis", "CA1822", Justification = "For test reasons")] + private class TestClassBase + { + public void BaseMethod() + { + throw TestExtensions.FalseException(); + } + + protected virtual void VirtualMethod() + { + throw TestExtensions.FalseException(); + } + } + + [SuppressMessage("Analysis", "CA1822", Justification = "For test reasons")] + private class DerivedClass : TestClassBase + { + public void DerivedMethod() + { + throw TestExtensions.TrueException(); + } + + protected override void VirtualMethod() + { + throw TestExtensions.TrueException(); + } + } + } +} \ No newline at end of file diff --git a/tests/Tests/Basics.Test/ObjectExtensionsDeepCopyTest.cs b/tests/Tests/Basics.Test/ObjectExtensionsDeepCopyTest.cs new file mode 100644 index 00000000..af7ef2a7 --- /dev/null +++ b/tests/Tests/Basics.Test/ObjectExtensionsDeepCopyTest.cs @@ -0,0 +1,237 @@ +namespace SpaceEngineers.Core.Basics.Test +{ + using System; + using System.Diagnostics.CodeAnalysis; + using System.Linq; + using System.Runtime.Serialization; + using Basics; + using DeepCopy; + using Xunit; + using Xunit.Abstractions; + + /// + /// DeepCopy test + /// + [SuppressMessage("Analysis", "SA1201", Justification = "For test reasons")] + public class ObjectExtensionsDeepCopyTest : BasicsTestBase + { + /// .ctor + /// ITestOutputHelper + public ObjectExtensionsDeepCopyTest(ITestOutputHelper output) + : base(output) { } + + [Fact] + internal void DeepCopyObjectTest() + { + var original = new object(); + var clone = original.DeepCopy(); + + Assert.NotEqual(original, clone); + Assert.False(ReferenceEquals(original, clone)); + + var instance1 = new object(); + var instance2 = new object(); + clone = instance1.DeepCopy(); + + Assert.True(instance1.GetType() == instance2.GetType()); + Assert.True(ReferenceEquals(instance1.GetType(), instance2.GetType())); + + Assert.True(instance1.GetType() == clone.GetType()); + Assert.True(ReferenceEquals(instance1.GetType(), clone.GetType())); + } + + [Fact] + internal void DeepCopyTest() + { + var original = TestReferenceWithSystemTypes.Create(); + var clone = original.DeepCopy(); + + AssertTestReferenceTypeWithTypes(original, clone, false); + } + + [Fact] + internal void DeepCopyBySerializationThrowsTest() + { + var original = TestReferenceWithSystemTypes.Create(); + + Assert.Throws(() => original.DeepCopyBySerialization()); + } + + [Fact] + internal void DeepCopyBySerializationTest() + { + var original = TestReferenceWithoutSystemTypes.CreateOrInit(); + var clone = original.DeepCopyBySerialization(); + + AssertTestReferenceTypeWithOutTypes(original, clone, true); + } + + private static void AssertTestReferenceTypeWithTypes(TestReferenceWithSystemTypes original, + TestReferenceWithSystemTypes clone, + bool bySerialization) + { + /* + * Type + */ + Assert.True(original.GetType() == clone.GetType()); + Assert.True(ReferenceEquals(original.GetType(), clone.GetType())); + Assert.True(original.Type == clone.Type); + Assert.True(ReferenceEquals(original.Type, clone.Type)); + + Assert.False(original.TypeArray?.Equals(clone.TypeArray)); + Assert.False(ReferenceEquals(original.TypeArray, clone.TypeArray)); + Assert.True(CheckReferenceArraySequentially(original.TypeArray, clone.TypeArray)); + + Assert.False(original.TypeCollection?.Equals(clone.TypeCollection)); + Assert.False(ReferenceEquals(original.TypeCollection, clone.TypeCollection)); + Assert.NotNull(original.TypeCollection); + Assert.NotNull(clone.TypeCollection); + Assert.True(original.TypeCollection != null + && clone.TypeCollection != null + && original.TypeCollection.SequenceEqual(clone.TypeCollection)); + + AssertTestReferenceTypeWithOutTypes(original, clone, bySerialization); + } + + private static void AssertTestReferenceTypeWithOutTypes(TestReferenceWithoutSystemTypes original, + TestReferenceWithoutSystemTypes clone, + bool bySerialization) + { + /* + * String + */ + Assert.Equal(original.String, clone.String); + Assert.True(bySerialization + ? !ReferenceEquals(original.String, clone.String) + : ReferenceEquals(original.String, clone.String)); + + /* + * ValueType + */ + Assert.Equal(original.Int, clone.Int); + + Assert.Equal(original.TestEnum, clone.TestEnum); + + Assert.False(original.ValueTypeArray?.Equals(clone.ValueTypeArray)); + Assert.False(ReferenceEquals(original.ValueTypeArray, clone.ValueTypeArray)); + Assert.True(CheckValueArraySequentially(original.ValueTypeArray, clone.ValueTypeArray)); + + Assert.False(original.ValueTypeCollection?.Equals(clone.ValueTypeCollection)); + Assert.False(ReferenceEquals(original.ValueTypeCollection, clone.ValueTypeCollection)); + Assert.NotNull(original.ValueTypeCollection); + Assert.NotNull(clone.ValueTypeCollection); + Assert.True(original.ValueTypeCollection != null + && clone.ValueTypeCollection != null + && original.ValueTypeCollection.SequenceEqual(clone.ValueTypeCollection)); + + /* + * ReferenceType + */ + Assert.False(original.ReferenceTypeArray?.Equals(clone.ReferenceTypeArray)); + Assert.False(ReferenceEquals(original.ReferenceTypeArray, clone.ReferenceTypeArray)); + Assert.False(CheckReferenceArraySequentially(original.ReferenceTypeArray, clone.ReferenceTypeArray)); + + Assert.False(original.ReferenceTypeCollection?.Equals(clone.ReferenceTypeCollection)); + Assert.False(ReferenceEquals(original.ReferenceTypeCollection, clone.ReferenceTypeCollection)); + Assert.NotNull(original.ReferenceTypeCollection); + Assert.NotNull(clone.ReferenceTypeCollection); + Assert.False(original.ReferenceTypeCollection != null + && clone.ReferenceTypeCollection != null + && original.ReferenceTypeCollection.SequenceEqual(clone.ReferenceTypeCollection)); + + Assert.True(original.Equals(original.CyclicReference)); + Assert.True(ReferenceEquals(original, original.CyclicReference)); + Assert.True(clone.Equals(clone.CyclicReference)); + Assert.True(ReferenceEquals(clone, clone.CyclicReference)); + + Assert.False(original.Equals(clone)); + Assert.False(ReferenceEquals(original, clone)); + + Assert.False(original.Equals(clone.CyclicReference)); + Assert.False(ReferenceEquals(original, clone.CyclicReference)); + + Assert.False(original.CyclicReference?.Equals(clone.CyclicReference)); + Assert.False(ReferenceEquals(original.CyclicReference, clone.CyclicReference)); + + Assert.True(original.Equals(TestReferenceWithoutSystemTypes.StaticCyclicReference)); + Assert.True(ReferenceEquals(original, TestReferenceWithoutSystemTypes.StaticCyclicReference)); + Assert.False(clone.Equals(TestReferenceWithoutSystemTypes.StaticCyclicReference)); + Assert.False(ReferenceEquals(clone, TestReferenceWithoutSystemTypes.StaticCyclicReference)); + + /* + * Nullable + */ + Assert.Null(clone.NullableInt); + Assert.Null(clone.NullableReference); + Assert.True(clone.CollectionOfNulls?.All(z => z == null)); + Assert.True(CheckArray(clone.ArrayOfNulls, null)); + } + + private static bool CheckArray(Array? array, object? value) + { + if (array == null) + { + return false; + } + + foreach (var item in array) + { + if (item != value) + { + return false; + } + } + + return true; + } + + [SuppressMessage("Analysis", "CA1508", Justification = "Analyzer error")] + private static bool CheckValueArraySequentially(Array? array, Array? compareWith) + where T : struct + { + if (array == null || compareWith == null) + { + return false; + } + + for (var i = 0; i < array.Length; ++i) + { + var left = (T?)array.GetValue(i); + var right = (T?)compareWith.GetValue(i); + + if (left == null + || right == null + || !((T)left).Equals((T)right)) + { + return false; + } + } + + return true; + } + + private static bool CheckReferenceArraySequentially(Array? array, Array? compareWith) + where T : class + { + if (array == null || compareWith == null) + { + return false; + } + + for (var i = 0; i < array.Length; ++i) + { + var left = (T?)array.GetValue(i); + var right = (T?)compareWith.GetValue(i); + + if (left == null + || right == null + || !left.Equals(right)) + { + return false; + } + } + + return true; + } + } +} \ No newline at end of file diff --git a/tests/Tests/Basics.Test/OrderByDependencyTestData.cs b/tests/Tests/Basics.Test/OrderByDependencyTestData.cs new file mode 100644 index 00000000..42e9b364 --- /dev/null +++ b/tests/Tests/Basics.Test/OrderByDependencyTestData.cs @@ -0,0 +1,64 @@ +namespace SpaceEngineers.Core.Basics.Test +{ + using Attributes; + + internal class OrderByDependencyTestData + { + /* + * non generic + */ + [After(typeof(DependencyTest2))] + internal class DependencyTest1 { } + + [After(typeof(DependencyTest3))] + internal class DependencyTest2 { } + + internal class DependencyTest3 { } + + [Before(typeof(DependencyTest3))] + internal class DependencyTest4 { } + + /* + * weakly typed + */ + [After("SpaceEngineers.Core.Basics.Test SpaceEngineers.Core.Basics.Test.OrderByDependencyTestData+WeakDependencyTest2")] + internal class WeakDependencyTest1 { } + + [After("SpaceEngineers.Core.Basics.Test SpaceEngineers.Core.Basics.Test.OrderByDependencyTestData+WeakDependencyTest3")] + internal class WeakDependencyTest2 { } + + internal class WeakDependencyTest3 { } + + [Before("SpaceEngineers.Core.Basics.Test SpaceEngineers.Core.Basics.Test.OrderByDependencyTestData+WeakDependencyTest3")] + internal class WeakDependencyTest4 { } + + /* + * generic + */ + [After(typeof(GenericDependencyTest2<>))] + internal class GenericDependencyTest1 { } + + [After(typeof(GenericDependencyTest3<>))] + internal class GenericDependencyTest2 { } + + internal class GenericDependencyTest3 { } + + [Before(typeof(GenericDependencyTest3<>))] + internal class GenericDependencyTest4 { } + + /* + * cycle dependency + */ + [After(typeof(CycleDependencyTest2))] + internal class CycleDependencyTest1 { } + + [After(typeof(CycleDependencyTest3))] + internal class CycleDependencyTest2 { } + + internal class CycleDependencyTest3 { } + + [Before(typeof(CycleDependencyTest3))] + [After(typeof(CycleDependencyTest1))] + internal class CycleDependencyTest4 { } + } +} \ No newline at end of file diff --git a/tests/Tests/Basics.Test/Properties/AssemblyInfo.cs b/tests/Tests/Basics.Test/Properties/AssemblyInfo.cs new file mode 100644 index 00000000..31adb06d --- /dev/null +++ b/tests/Tests/Basics.Test/Properties/AssemblyInfo.cs @@ -0,0 +1,5 @@ +using System.Reflection; + +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] +[assembly: AssemblyInformationalVersion("1.0.0.0")] \ No newline at end of file diff --git a/tests/Tests/Basics.Test/StreamExtensionsTest.cs b/tests/Tests/Basics.Test/StreamExtensionsTest.cs new file mode 100644 index 00000000..6958679c --- /dev/null +++ b/tests/Tests/Basics.Test/StreamExtensionsTest.cs @@ -0,0 +1,99 @@ +namespace SpaceEngineers.Core.Basics.Test +{ + using System; + using System.Globalization; + using System.Text; + using System.Threading; + using System.Threading.Tasks; + using Xunit; + using Xunit.Abstractions; + + /// + /// StreamExtensions class test + /// + public class StreamExtensionsTest : BasicsTestBase + { + /// .ctor + /// ITestOutputHelper + public StreamExtensionsTest(ITestOutputHelper output) + : base(output) { } + + [Fact] + internal void OverwriteTest() + { + const string str1 = "Hello world!"; + const string str2 = "Hello!"; + + Assert.NotEqual(str1, str2); + + var encoding = Encoding.UTF8; + + ReadOnlySpan bytes = encoding.GetBytes(str1); + + using (var stream = bytes.AsMemoryStream()) + { + var read = stream.AsString(encoding); + Assert.Equal(str1, read); + + stream.Overwrite(encoding.GetBytes(str2)); + + read = stream.AsString(encoding); + Assert.Equal(str2, read); + } + } + + [Fact] + internal async Task OverwriteAsyncTest() + { + const string str1 = "Hello world!"; + const string str2 = "Hello!"; + + Assert.NotEqual(str1, str2); + + var encoding = Encoding.UTF8; + + ReadOnlyMemory bytes = encoding.GetBytes(str1); + + using (var cts = new CancellationTokenSource(TimeSpan.FromSeconds(3))) + using (var stream = bytes.Span.AsMemoryStream()) + { + var read = await stream.AsString(encoding, cts.Token).ConfigureAwait(false); + Assert.Equal(str1, read); + + await stream.Overwrite(encoding.GetBytes(str2), cts.Token).ConfigureAwait(false); + + read = await stream.AsString(encoding, cts.Token).ConfigureAwait(false); + Assert.Equal(str2, read); + } + } + + [Fact] + internal void CompressionTest() + { + var sb = new StringBuilder(); + + for (var i = 0; i < 42; i++) + { + sb.Append(nameof(CompressionTest)); + } + + var repeatedString = sb.ToString(); + ReadOnlySpan bytes = Encoding.UTF8.GetBytes(repeatedString); + + ReadOnlySpan compressedBytes = bytes.Compress().Span; + + Output.WriteLine(bytes.Length.ToString(CultureInfo.InvariantCulture)); + Output.WriteLine(compressedBytes.Length.ToString(CultureInfo.InvariantCulture)); + Assert.True(bytes.Length > compressedBytes.Length); + + ReadOnlySpan decompressedBytes = compressedBytes.Decompress().Span; + + Output.WriteLine(decompressedBytes.Length.ToString(CultureInfo.InvariantCulture)); + Assert.Equal(bytes.Length, decompressedBytes.Length); + + var decompressedRepeatedString = Encoding.UTF8.GetString(decompressedBytes); + + Assert.Equal(repeatedString, decompressedRepeatedString); + } + } +} \ No newline at end of file diff --git a/tests/Tests/Basics.Test/StringExtensionsTest.cs b/tests/Tests/Basics.Test/StringExtensionsTest.cs new file mode 100644 index 00000000..e0f583f4 --- /dev/null +++ b/tests/Tests/Basics.Test/StringExtensionsTest.cs @@ -0,0 +1,23 @@ +namespace SpaceEngineers.Core.Basics.Test +{ + using Xunit; + using Xunit.Abstractions; + + /// + /// StringExtensions class test + /// + public class StringExtensionsTest : BasicsTestBase + { + /// .ctor + /// ITestOutputHelper + public StringExtensionsTest(ITestOutputHelper output) + : base(output) { } + + [Theory] + [InlineData("qwerty", "Qwerty")] + internal void StartFromCapitalLetterTest(string source, string expected) + { + Assert.Equal(expected, source.StartFromCapitalLetter()); + } + } +} \ No newline at end of file diff --git a/tests/Tests/Basics.Test/TestExtensions.cs b/tests/Tests/Basics.Test/TestExtensions.cs new file mode 100644 index 00000000..09ddd258 --- /dev/null +++ b/tests/Tests/Basics.Test/TestExtensions.cs @@ -0,0 +1,24 @@ +namespace SpaceEngineers.Core.Basics.Test +{ + using Xunit.Sdk; + + /// + /// extensions for tests + /// + public static class TestExtensions + { + /// Create FalseException + /// FalseException + public static FalseException FalseException() + { + return Xunit.Sdk.FalseException.ForNonFalseValue(nameof(FalseException), null); + } + + /// Create TrueException + /// TrueException + public static TrueException TrueException() + { + return Xunit.Sdk.TrueException.ForNonTrueValue(nameof(TrueException), null); + } + } +} \ No newline at end of file diff --git a/tests/Tests/Basics.Test/TestRecord.cs b/tests/Tests/Basics.Test/TestRecord.cs new file mode 100644 index 00000000..036b9be7 --- /dev/null +++ b/tests/Tests/Basics.Test/TestRecord.cs @@ -0,0 +1,7 @@ +namespace SpaceEngineers.Core.Basics.Test +{ + internal record TestRecord + { + public string StringValue { get; init; } = default!; + } +} \ No newline at end of file diff --git a/tests/Tests/Basics.Test/TypeExtensionsTest.cs b/tests/Tests/Basics.Test/TypeExtensionsTest.cs new file mode 100644 index 00000000..7f5af598 --- /dev/null +++ b/tests/Tests/Basics.Test/TypeExtensionsTest.cs @@ -0,0 +1,364 @@ +namespace SpaceEngineers.Core.Basics.Test +{ + using System; + using System.Collections.Generic; + using System.Diagnostics.CodeAnalysis; + using System.Linq; + using Basics; + using Xunit; + using Xunit.Abstractions; + + /// + /// TypeExtensions class tests + /// + [SuppressMessage("Analysis", "SA1201", Justification = "For test reasons")] + public class TypeExtensionsTest : BasicsTestBase + { + /// .ctor + /// ITestOutputHelper + public TypeExtensionsTest(ITestOutputHelper output) + : base(output) + { + } + + [Theory] + [InlineData(typeof(object), false)] + [InlineData(typeof(string), false)] + [InlineData(typeof(int), false)] + [InlineData(typeof(KeyValuePair), false)] + [InlineData(typeof(TestRecord), true)] + internal void IsRecordTest(Type type, bool result) + { + Assert.Equal(result, type.IsRecord()); + } + + [Theory] + [InlineData(typeof(string))] + [InlineData(typeof(SpaceEngineers.Core.Basics.Primitives.AsyncManualResetEvent))] + [InlineData(typeof(SpaceEngineers.Core.Basics.Primitives.PriorityQueue<,>))] + [InlineData(typeof(SpaceEngineers.Core.Basics.Primitives.PriorityQueue))] + [InlineData(typeof(SpaceEngineers.Core.Basics.Primitives.PriorityQueue, string>))] + [InlineData(typeof(SpaceEngineers.Core.Basics.Primitives.PriorityQueue>, string>))] + [InlineData(typeof(SpaceEngineers.Core.Basics.Primitives.PriorityQueue, string>))] + internal void FindTypeByFullNameTest(Type type) + { + var typeNode = TypeNode.FromType(type); + + Output.WriteLine(typeNode.ToString()); + + typeNode = TypeNode.FromString(typeNode.ToString()); + + Type builtType = typeNode; + + Assert.Equal(type, builtType); + } + + [Fact] + internal void OrderByDependencyCycleDependencyTest() + { + var test1 = new[] + { + typeof(OrderByDependencyTestData.CycleDependencyTest1), + typeof(OrderByDependencyTestData.CycleDependencyTest2), + typeof(OrderByDependencyTestData.CycleDependencyTest3) + }; + + Assert.Throws(() => test1.OrderByDependencies().ToArray()); + + var test2 = new[] + { + typeof(OrderByDependencyTestData.CycleDependencyTest1), + typeof(OrderByDependencyTestData.CycleDependencyTest2), + typeof(OrderByDependencyTestData.CycleDependencyTest3), + typeof(OrderByDependencyTestData.CycleDependencyTest4) + }; + + Assert.Throws(() => test2.OrderByDependencies().ToArray()); + } + + [Fact] + internal void OrderByDependencyTest() + { + var test1 = new[] + { + typeof(OrderByDependencyTestData.DependencyTest1), + typeof(OrderByDependencyTestData.DependencyTest2), + typeof(OrderByDependencyTestData.DependencyTest3), + typeof(OrderByDependencyTestData.DependencyTest4) + }; + + Assert.True(test1.Reverse().SequenceEqual(test1.OrderByDependencies())); + + var test2 = new[] + { + typeof(OrderByDependencyTestData.DependencyTest1), + typeof(OrderByDependencyTestData.DependencyTest2), + typeof(OrderByDependencyTestData.DependencyTest3), + typeof(OrderByDependencyTestData.DependencyTest4) + }; + + Assert.True(test2.Reverse().SequenceEqual(test2.OrderByDependencies())); + + var test3 = new[] + { + typeof(OrderByDependencyTestData.GenericDependencyTest1<>), + typeof(OrderByDependencyTestData.GenericDependencyTest2<>), + typeof(OrderByDependencyTestData.GenericDependencyTest3<>), + typeof(OrderByDependencyTestData.GenericDependencyTest4<>) + }; + + Assert.True(test3.Reverse().SequenceEqual(test3.OrderByDependencies())); + + var test4 = new[] + { + typeof(OrderByDependencyTestData.GenericDependencyTest1), + typeof(OrderByDependencyTestData.GenericDependencyTest2), + typeof(OrderByDependencyTestData.GenericDependencyTest3), + typeof(OrderByDependencyTestData.GenericDependencyTest4) + }; + + Assert.True(test4.Reverse().SequenceEqual(test4.OrderByDependencies())); + } + + [Fact] + internal void IsNullableTest() + { + Assert.False(typeof(bool).IsNullable()); + Assert.True(typeof(bool?).IsNullable()); + Assert.True(typeof(Nullable<>).IsNullable()); + + Assert.False(typeof(string).IsNullable()); + Assert.False(typeof(object).IsNullable()); + + var nullableString = (string?)string.Empty; + var nullableObject = (object?)string.Empty; + + Assert.False(nullableString.GetType().IsNullable()); + Assert.False(nullableObject.GetType().IsNullable()); + } + + [Fact] + internal void IsNullableMemberTest() + { + var notNullableValueField = typeof(ClassWithNullableMembers).GetField(nameof(ClassWithNullableMembers._notNullableValue)); + var nullableValueField = typeof(ClassWithNullableMembers).GetField(nameof(ClassWithNullableMembers._nullableValue)); + var notNullableReferenceField = typeof(ClassWithNullableMembers).GetField(nameof(ClassWithNullableMembers._notNullableReference)); + var nullableReferenceField = typeof(ClassWithNullableMembers).GetField(nameof(ClassWithNullableMembers._nullableReference)); + + Assert.NotNull(notNullableValueField); + Assert.NotNull(nullableValueField); + Assert.NotNull(notNullableReferenceField); + Assert.NotNull(nullableReferenceField); + + Assert.False(notNullableValueField!.IsNullable()); + Assert.True(nullableValueField!.IsNullable()); + Assert.False(notNullableReferenceField!.IsNullable()); + Assert.True(nullableReferenceField!.IsNullable()); + + var notNullableValueProperty = typeof(ClassWithNullableMembers).GetProperty(nameof(ClassWithNullableMembers.NotNullableValue)); + var nullableValueProperty = typeof(ClassWithNullableMembers).GetProperty(nameof(ClassWithNullableMembers.NullableValue)); + var notNullableReferenceProperty = typeof(ClassWithNullableMembers).GetProperty(nameof(ClassWithNullableMembers.NotNullableReference)); + var nullableReferenceProperty = typeof(ClassWithNullableMembers).GetProperty(nameof(ClassWithNullableMembers.NullableReference)); + + Assert.NotNull(notNullableValueProperty); + Assert.NotNull(nullableValueProperty); + Assert.NotNull(notNullableReferenceProperty); + Assert.NotNull(nullableReferenceProperty); + + Assert.False(notNullableValueProperty!.IsNullable()); + Assert.True(nullableValueProperty!.IsNullable()); + Assert.False(notNullableReferenceProperty!.IsNullable()); + Assert.True(nullableReferenceProperty!.IsNullable()); + } + + [Fact] + internal void IsSubclassOfOpenGenericTest() + { + // ITestInterface + Assert.False(typeof(ITestGenericInterfaceBase<>).IsSubclassOfOpenGeneric(typeof(ITestInterface))); + Assert.False(typeof(TestTypeImplementation).IsSubclassOfOpenGeneric(typeof(ITestInterface))); + Assert.False(typeof(ITestGenericInterface<>).IsSubclassOfOpenGeneric(typeof(ITestInterface))); + Assert.False(typeof(TestGenericTypeImplementationBase<>).IsSubclassOfOpenGeneric(typeof(ITestInterface))); + + // ITestGenericInterfaceBase + Assert.True(typeof(ITestGenericInterfaceBase<>).IsSubclassOfOpenGeneric(typeof(ITestGenericInterfaceBase<>))); + Assert.True(typeof(ITestGenericInterface<>).IsSubclassOfOpenGeneric(typeof(ITestGenericInterfaceBase<>))); + Assert.True(typeof(TestGenericTypeImplementationBase<>).IsSubclassOfOpenGeneric(typeof(ITestGenericInterfaceBase<>))); + Assert.True(typeof(TestTypeImplementation).IsSubclassOfOpenGeneric(typeof(ITestGenericInterfaceBase<>))); + Assert.True(typeof(TestGenericTypeImplementation<>).IsSubclassOfOpenGeneric(typeof(ITestGenericInterfaceBase<>))); + + // ITestGenericInterface + Assert.False(typeof(ITestGenericInterface<>).IsSubclassOfOpenGeneric(typeof(ITestGenericInterface))); + Assert.False(typeof(TestGenericTypeImplementationBase<>).IsSubclassOfOpenGeneric(typeof(ITestGenericInterface))); + Assert.False(typeof(TestTypeImplementation).IsSubclassOfOpenGeneric(typeof(ITestGenericInterface))); + Assert.False(typeof(TestGenericTypeImplementation<>).IsSubclassOfOpenGeneric(typeof(ITestGenericInterface))); + } + + [Fact] + internal void IsContainsInterfaceDeclarationTest() + { + Assert.True(typeof(ITestGenericInterfaceBase).IsContainsInterfaceDeclaration(typeof(ITestInterface))); + Assert.False(typeof(ITestGenericInterface).IsContainsInterfaceDeclaration(typeof(ITestInterface))); + + Assert.False(typeof(ITestInterface).IsContainsInterfaceDeclaration(typeof(ITestInterface))); + Assert.False(typeof(TestTypeImplementation).IsContainsInterfaceDeclaration(typeof(ITestInterface))); + + Assert.False(typeof(TestGenericTypeImplementationBase).IsContainsInterfaceDeclaration(typeof(ITestGenericInterface))); + Assert.False(typeof(ITestGenericInterface).IsContainsInterfaceDeclaration(typeof(ITestGenericInterfaceBase))); + Assert.False(typeof(DirectTestTypeImplementation).IsContainsInterfaceDeclaration(typeof(ITestGenericInterface))); + + Assert.True(typeof(TestGenericTypeImplementationBase).IsContainsInterfaceDeclaration(typeof(ITestGenericInterface))); + Assert.True(typeof(ITestGenericInterface).IsContainsInterfaceDeclaration(typeof(ITestGenericInterfaceBase))); + Assert.True(typeof(DirectTestTypeImplementation).IsContainsInterfaceDeclaration(typeof(ITestGenericInterface))); + + Assert.False(typeof(TestGenericTypeImplementationBase).IsContainsInterfaceDeclaration(typeof(ITestGenericInterface<>))); + Assert.False(typeof(ITestGenericInterface).IsContainsInterfaceDeclaration(typeof(ITestGenericInterfaceBase<>))); + Assert.False(typeof(DirectTestTypeImplementation).IsContainsInterfaceDeclaration(typeof(ITestGenericInterface<>))); + } + + [Fact] + internal void ExtractGenericArgumentsAtTest() + { + Assert.Equal(typeof(string), typeof(ITestGenericInterface).ExtractGenericArgumentsAt(typeof(ITestGenericInterfaceBase<>), 0).Single()); + Assert.Equal(typeof(object), typeof(TestTypeImplementation).ExtractGenericArgumentsAt(typeof(ITestGenericInterfaceBase<>), 0).Single()); + Assert.Equal(typeof(object), typeof(DirectTestTypeImplementation).ExtractGenericArgumentsAt(typeof(ITestGenericInterfaceBase<>), 0).Single()); + Assert.Equal(typeof(bool), typeof(TestGenericTypeImplementation).ExtractGenericArgumentsAt(typeof(TestGenericTypeImplementationBase<>), 0).Single()); + + Assert.Equal(typeof(bool), typeof(ClosedImplementation).ExtractGenericArgumentsAt(typeof(ITestInterface<,>), 0).Single()); + Assert.Equal(typeof(object), typeof(ClosedImplementation).ExtractGenericArgumentsAt(typeof(ITestInterface<,>), 1).Single()); + Assert.Equal("T1", typeof(OpenedImplementation<,>).ExtractGenericArgumentsAt(typeof(ITestInterface<,>), 0).Single().Name); + Assert.Equal("T2", typeof(OpenedImplementation<,>).ExtractGenericArgumentsAt(typeof(ITestInterface<,>), 1).Single().Name); + Assert.Equal("T1", typeof(HalfOpenedImplementation<>).ExtractGenericArgumentsAt(typeof(ITestInterface<,>), 0).Single().Name); + Assert.Equal(typeof(object), typeof(HalfOpenedImplementation<>).ExtractGenericArgumentsAt(typeof(ITestInterface<,>), 1).Single()); + Assert.True(new List { typeof(bool), typeof(string) }.SequenceEqual(typeof(SeveralImplementations).ExtractGenericArgumentsAt(typeof(ITestInterface<,>), 0).ToList())); + Assert.True(new List { typeof(object), typeof(int) }.SequenceEqual(typeof(SeveralImplementations).ExtractGenericArgumentsAt(typeof(ITestInterface<,>), 1).ToList())); + + Assert.Throws(() => typeof(ITestGenericInterface).ExtractGenericArgumentsAt(typeof(ITestGenericInterfaceBase), 0).Any()); + Assert.Throws(() => typeof(TestTypeImplementation).ExtractGenericArgumentsAt(typeof(ITestGenericInterfaceBase), 0).Any()); + Assert.Throws(() => typeof(DirectTestTypeImplementation).ExtractGenericArgumentsAt(typeof(ITestGenericInterfaceBase), 0).Any()); + Assert.Throws(() => typeof(TestGenericTypeImplementation).ExtractGenericArgumentsAt(typeof(TestGenericTypeImplementationBase), 0).Any()); + } + + [Fact] + internal void FitsForTypeArgumentTest() + { + Assert.True(typeof(bool).FitsForTypeArgument(typeof(ITestInterface<,>).GetGenericArguments()[0])); + Assert.True(typeof(Enum).FitsForTypeArgument(typeof(ITestInterface<,>).GetGenericArguments()[0])); + Assert.True(typeof(StringSplitOptions).FitsForTypeArgument(typeof(ITestInterface<,>).GetGenericArguments()[0])); + Assert.True(typeof(StructWithParameter).FitsForTypeArgument(typeof(ITestInterface<,>).GetGenericArguments()[0])); + Assert.True(typeof(object).FitsForTypeArgument(typeof(ITestInterface<,>).GetGenericArguments()[0])); + Assert.True(typeof(ClassWithParameter).FitsForTypeArgument(typeof(ITestInterface<,>).GetGenericArguments()[0])); + + Assert.True(typeof(bool).FitsForTypeArgument(typeof(ITestInterface<,>).GetGenericArguments()[1])); + Assert.True(typeof(Enum).FitsForTypeArgument(typeof(ITestInterface<,>).GetGenericArguments()[1])); + Assert.True(typeof(StringSplitOptions).FitsForTypeArgument(typeof(ITestInterface<,>).GetGenericArguments()[1])); + Assert.True(typeof(StructWithParameter).FitsForTypeArgument(typeof(ITestInterface<,>).GetGenericArguments()[1])); + Assert.True(typeof(object).FitsForTypeArgument(typeof(ITestInterface<,>).GetGenericArguments()[1])); + Assert.True(typeof(ClassWithParameter).FitsForTypeArgument(typeof(ITestInterface<,>).GetGenericArguments()[1])); + + Assert.False(typeof(bool).FitsForTypeArgument(typeof(IClassConstrained<>).GetGenericArguments()[0])); + Assert.True(typeof(Enum).FitsForTypeArgument(typeof(IClassConstrained<>).GetGenericArguments()[0])); + Assert.False(typeof(StringSplitOptions).FitsForTypeArgument(typeof(IClassConstrained<>).GetGenericArguments()[0])); + Assert.False(typeof(StructWithParameter).FitsForTypeArgument(typeof(IClassConstrained<>).GetGenericArguments()[0])); + Assert.True(typeof(object).FitsForTypeArgument(typeof(IClassConstrained<>).GetGenericArguments()[0])); + Assert.True(typeof(ClassWithParameter).FitsForTypeArgument(typeof(IClassConstrained<>).GetGenericArguments()[0])); + + Assert.True(typeof(bool).FitsForTypeArgument(typeof(IStructConstrained<>).GetGenericArguments()[0])); + Assert.False(typeof(Enum).FitsForTypeArgument(typeof(IStructConstrained<>).GetGenericArguments()[0])); + Assert.True(typeof(StringSplitOptions).FitsForTypeArgument(typeof(IStructConstrained<>).GetGenericArguments()[0])); + Assert.True(typeof(StructWithParameter).FitsForTypeArgument(typeof(IStructConstrained<>).GetGenericArguments()[0])); + Assert.False(typeof(object).FitsForTypeArgument(typeof(IStructConstrained<>).GetGenericArguments()[0])); + Assert.False(typeof(ClassWithParameter).FitsForTypeArgument(typeof(IStructConstrained<>).GetGenericArguments()[0])); + + Assert.True(typeof(bool).FitsForTypeArgument(typeof(IDefaultCtorConstrained<>).GetGenericArguments()[0])); + Assert.False(typeof(Enum).FitsForTypeArgument(typeof(IDefaultCtorConstrained<>).GetGenericArguments()[0])); + Assert.True(typeof(StringSplitOptions).FitsForTypeArgument(typeof(IDefaultCtorConstrained<>).GetGenericArguments()[0])); + Assert.True(typeof(StructWithParameter).FitsForTypeArgument(typeof(IDefaultCtorConstrained<>).GetGenericArguments()[0])); + Assert.True(typeof(object).FitsForTypeArgument(typeof(IDefaultCtorConstrained<>).GetGenericArguments()[0])); + Assert.False(typeof(ClassWithParameter).FitsForTypeArgument(typeof(IDefaultCtorConstrained<>).GetGenericArguments()[0])); + + Assert.True(typeof(HalfOpenedImplementation).FitsForTypeArgument(typeof(ITestInterface))); + } + + private interface ITestInterface { } + + private interface ITestInterface { } + + private interface ITestGenericInterfaceBase : ITestInterface { } + + private interface ITestGenericInterface : ITestGenericInterfaceBase, ITestInterface { } + + private abstract class TestGenericTypeImplementationBase : ITestGenericInterface { } + + private class DirectTestTypeImplementation : ITestGenericInterface { } + + private class TestTypeImplementation : TestGenericTypeImplementationBase { } + + private class TestGenericTypeImplementation : TestGenericTypeImplementationBase { } + + private class ClosedImplementation : ITestInterface { } + + private class OpenedImplementation : ITestInterface { } + + private class HalfOpenedImplementation : ITestInterface { } + + private class SeveralImplementations : ITestInterface, ITestInterface { } + + private interface IClassConstrained + where T : class { } + + private interface IStructConstrained + where T : struct { } + + private interface IDefaultCtorConstrained + where T : new() { } + + [SuppressMessage("Analysis", "CA1801", Justification = "For test reasons")] + private class ClassWithParameter + { + public ClassWithParameter(object param) { } + } + + [SuppressMessage("Analysis", "CA1801", Justification = "For test reasons")] + private struct StructWithParameter + { + public StructWithParameter(object param) { } + } + + [SuppressMessage("Analysis", "SA1401", Justification = "For test reasons")] + private class ClassWithNullableMembers + { + public bool _notNullableValue; + + public bool? _nullableValue; + + public object _notNullableReference = null!; + + public object? _nullableReference; + + public bool NotNullableValue + { + get => _notNullableValue; + set => _notNullableValue = value; + } + + public bool? NullableValue + { + get => _nullableValue; + set => _nullableValue = value; + } + + public object NotNullableReference + { + get => _notNullableReference; + set => _notNullableReference = value; + } + + public object? NullableReference + { + get => _nullableReference; + set => _nullableReference = value; + } + } + } +} \ No newline at end of file diff --git a/tests/Tests/CompositionRoot.Test/AutoRegistrationTest/BaseUnregisteredService.cs b/tests/Tests/CompositionRoot.Test/AutoRegistrationTest/BaseUnregisteredService.cs new file mode 100644 index 00000000..d387f78b --- /dev/null +++ b/tests/Tests/CompositionRoot.Test/AutoRegistrationTest/BaseUnregisteredService.cs @@ -0,0 +1,11 @@ +namespace SpaceEngineers.Core.CompositionRoot.Test.AutoRegistrationTest +{ + using AutoRegistration.Api.Abstractions; + using AutoRegistration.Api.Attributes; + + [UnregisteredComponent] + internal class BaseUnregisteredService : IUnregisteredService, + IResolvable + { + } +} \ No newline at end of file diff --git a/tests/Tests/CompositionRoot.Test/AutoRegistrationTest/ClosedGenericImplementationOfOpenGenericService.cs b/tests/Tests/CompositionRoot.Test/AutoRegistrationTest/ClosedGenericImplementationOfOpenGenericService.cs new file mode 100644 index 00000000..c65ef6f1 --- /dev/null +++ b/tests/Tests/CompositionRoot.Test/AutoRegistrationTest/ClosedGenericImplementationOfOpenGenericService.cs @@ -0,0 +1,12 @@ +namespace SpaceEngineers.Core.CompositionRoot.Test.AutoRegistrationTest +{ + using AutoRegistration.Api.Abstractions; + using AutoRegistration.Api.Attributes; + using AutoRegistration.Api.Enumerations; + + [Component(EnLifestyle.Transient)] + internal class ClosedGenericImplementationOfOpenGenericService : IOpenGenericTestService, + IResolvable> + { + } +} \ No newline at end of file diff --git a/tests/Tests/CompositionRoot.Test/AutoRegistrationTest/CollectionResolvableTestServiceImpl1.cs b/tests/Tests/CompositionRoot.Test/AutoRegistrationTest/CollectionResolvableTestServiceImpl1.cs new file mode 100644 index 00000000..a6425811 --- /dev/null +++ b/tests/Tests/CompositionRoot.Test/AutoRegistrationTest/CollectionResolvableTestServiceImpl1.cs @@ -0,0 +1,12 @@ +namespace SpaceEngineers.Core.CompositionRoot.Test.AutoRegistrationTest +{ + using AutoRegistration.Api.Attributes; + using AutoRegistration.Api.Enumerations; + using Basics.Attributes; + + [Component(EnLifestyle.Transient)] + [After(typeof(CollectionResolvableTestServiceImpl2))] + internal class CollectionResolvableTestServiceImpl1 : ICollectionResolvableTestService + { + } +} \ No newline at end of file diff --git a/tests/Tests/CompositionRoot.Test/AutoRegistrationTest/CollectionResolvableTestServiceImpl2.cs b/tests/Tests/CompositionRoot.Test/AutoRegistrationTest/CollectionResolvableTestServiceImpl2.cs new file mode 100644 index 00000000..179d7d80 --- /dev/null +++ b/tests/Tests/CompositionRoot.Test/AutoRegistrationTest/CollectionResolvableTestServiceImpl2.cs @@ -0,0 +1,12 @@ +namespace SpaceEngineers.Core.CompositionRoot.Test.AutoRegistrationTest +{ + using AutoRegistration.Api.Attributes; + using AutoRegistration.Api.Enumerations; + using Basics.Attributes; + + [Component(EnLifestyle.Transient)] + [After(typeof(CollectionResolvableTestServiceImpl3))] + internal class CollectionResolvableTestServiceImpl2 : ICollectionResolvableTestService + { + } +} \ No newline at end of file diff --git a/tests/Tests/CompositionRoot.Test/AutoRegistrationTest/CollectionResolvableTestServiceImpl3.cs b/tests/Tests/CompositionRoot.Test/AutoRegistrationTest/CollectionResolvableTestServiceImpl3.cs new file mode 100644 index 00000000..dc6a289a --- /dev/null +++ b/tests/Tests/CompositionRoot.Test/AutoRegistrationTest/CollectionResolvableTestServiceImpl3.cs @@ -0,0 +1,10 @@ +namespace SpaceEngineers.Core.CompositionRoot.Test.AutoRegistrationTest +{ + using AutoRegistration.Api.Attributes; + using AutoRegistration.Api.Enumerations; + + [Component(EnLifestyle.Transient)] + internal class CollectionResolvableTestServiceImpl3 : ICollectionResolvableTestService + { + } +} \ No newline at end of file diff --git a/tests/Tests/CompositionRoot.Test/AutoRegistrationTest/ConcreteImplementationGenericService.cs b/tests/Tests/CompositionRoot.Test/AutoRegistrationTest/ConcreteImplementationGenericService.cs new file mode 100644 index 00000000..b143e010 --- /dev/null +++ b/tests/Tests/CompositionRoot.Test/AutoRegistrationTest/ConcreteImplementationGenericService.cs @@ -0,0 +1,14 @@ +namespace SpaceEngineers.Core.CompositionRoot.Test.AutoRegistrationTest +{ + using AutoRegistration.Api.Abstractions; + using AutoRegistration.Api.Attributes; + using AutoRegistration.Api.Enumerations; + + [Component(EnLifestyle.Singleton)] + internal class ConcreteImplementationGenericService : IResolvable> + { + public ConcreteImplementationGenericService() + { + } + } +} \ No newline at end of file diff --git a/tests/Tests/CompositionRoot.Test/AutoRegistrationTest/ConcreteImplementationService.cs b/tests/Tests/CompositionRoot.Test/AutoRegistrationTest/ConcreteImplementationService.cs new file mode 100644 index 00000000..d1131920 --- /dev/null +++ b/tests/Tests/CompositionRoot.Test/AutoRegistrationTest/ConcreteImplementationService.cs @@ -0,0 +1,14 @@ +namespace SpaceEngineers.Core.CompositionRoot.Test.AutoRegistrationTest +{ + using AutoRegistration.Api.Abstractions; + using AutoRegistration.Api.Attributes; + using AutoRegistration.Api.Enumerations; + + [Component(EnLifestyle.Singleton)] + internal class ConcreteImplementationService : IResolvable + { + public ConcreteImplementationService() + { + } + } +} \ No newline at end of file diff --git a/tests/Tests/CompositionRoot.Test/AutoRegistrationTest/ConcreteImplementationWithDependencyService.cs b/tests/Tests/CompositionRoot.Test/AutoRegistrationTest/ConcreteImplementationWithDependencyService.cs new file mode 100644 index 00000000..dbcf9442 --- /dev/null +++ b/tests/Tests/CompositionRoot.Test/AutoRegistrationTest/ConcreteImplementationWithDependencyService.cs @@ -0,0 +1,17 @@ +namespace SpaceEngineers.Core.CompositionRoot.Test.AutoRegistrationTest +{ + using AutoRegistration.Api.Abstractions; + using AutoRegistration.Api.Attributes; + using AutoRegistration.Api.Enumerations; + + [Component(EnLifestyle.Transient)] + internal class ConcreteImplementationWithDependencyService : IResolvable + { + public ConcreteImplementationWithDependencyService(ConcreteImplementationService dependency) + { + Dependency = dependency; + } + + public ConcreteImplementationService Dependency { get; } + } +} \ No newline at end of file diff --git a/tests/Tests/CompositionRoot.Test/AutoRegistrationTest/DecorableService.cs b/tests/Tests/CompositionRoot.Test/AutoRegistrationTest/DecorableService.cs new file mode 100644 index 00000000..13baa1bb --- /dev/null +++ b/tests/Tests/CompositionRoot.Test/AutoRegistrationTest/DecorableService.cs @@ -0,0 +1,12 @@ +namespace SpaceEngineers.Core.CompositionRoot.Test.AutoRegistrationTest +{ + using AutoRegistration.Api.Abstractions; + using AutoRegistration.Api.Attributes; + using AutoRegistration.Api.Enumerations; + + [Component(EnLifestyle.Transient)] + internal class DecorableService : IDecorableService, + IResolvable + { + } +} \ No newline at end of file diff --git a/tests/Tests/CompositionRoot.Test/AutoRegistrationTest/DecorableServiceDecorator1.cs b/tests/Tests/CompositionRoot.Test/AutoRegistrationTest/DecorableServiceDecorator1.cs new file mode 100644 index 00000000..01412e19 --- /dev/null +++ b/tests/Tests/CompositionRoot.Test/AutoRegistrationTest/DecorableServiceDecorator1.cs @@ -0,0 +1,19 @@ +namespace SpaceEngineers.Core.CompositionRoot.Test.AutoRegistrationTest +{ + using AutoRegistration.Api.Attributes; + using AutoRegistration.Api.Enumerations; + using Basics.Attributes; + + [Component(EnLifestyle.Transient)] + [After(typeof(DecorableServiceDecorator2))] + internal class DecorableServiceDecorator1 : IDecorableService, + IDecorableServiceDecorator + { + public DecorableServiceDecorator1(IDecorableService decoratorType) + { + Decoratee = decoratorType; + } + + public IDecorableService Decoratee { get; } + } +} \ No newline at end of file diff --git a/tests/Tests/CompositionRoot.Test/AutoRegistrationTest/DecorableServiceDecorator2.cs b/tests/Tests/CompositionRoot.Test/AutoRegistrationTest/DecorableServiceDecorator2.cs new file mode 100644 index 00000000..ab803d65 --- /dev/null +++ b/tests/Tests/CompositionRoot.Test/AutoRegistrationTest/DecorableServiceDecorator2.cs @@ -0,0 +1,19 @@ +namespace SpaceEngineers.Core.CompositionRoot.Test.AutoRegistrationTest +{ + using AutoRegistration.Api.Attributes; + using AutoRegistration.Api.Enumerations; + using Basics.Attributes; + + [Component(EnLifestyle.Transient)] + [After(typeof(DecorableServiceDecorator3))] + internal class DecorableServiceDecorator2 : IDecorableService, + IDecorableServiceDecorator + { + public DecorableServiceDecorator2(IDecorableService decoratorType) + { + Decoratee = decoratorType; + } + + public IDecorableService Decoratee { get; } + } +} \ No newline at end of file diff --git a/tests/Tests/CompositionRoot.Test/AutoRegistrationTest/DecorableServiceDecorator3.cs b/tests/Tests/CompositionRoot.Test/AutoRegistrationTest/DecorableServiceDecorator3.cs new file mode 100644 index 00000000..589ebf38 --- /dev/null +++ b/tests/Tests/CompositionRoot.Test/AutoRegistrationTest/DecorableServiceDecorator3.cs @@ -0,0 +1,17 @@ +namespace SpaceEngineers.Core.CompositionRoot.Test.AutoRegistrationTest +{ + using AutoRegistration.Api.Attributes; + using AutoRegistration.Api.Enumerations; + + [Component(EnLifestyle.Transient)] + internal class DecorableServiceDecorator3 : IDecorableService, + IDecorableServiceDecorator + { + public DecorableServiceDecorator3(IDecorableService decoratorType) + { + Decoratee = decoratorType; + } + + public IDecorableService Decoratee { get; } + } +} \ No newline at end of file diff --git a/tests/Tests/CompositionRoot.Test/AutoRegistrationTest/DerivedFromUnregisteredService.cs b/tests/Tests/CompositionRoot.Test/AutoRegistrationTest/DerivedFromUnregisteredService.cs new file mode 100644 index 00000000..025dce21 --- /dev/null +++ b/tests/Tests/CompositionRoot.Test/AutoRegistrationTest/DerivedFromUnregisteredService.cs @@ -0,0 +1,10 @@ +namespace SpaceEngineers.Core.CompositionRoot.Test.AutoRegistrationTest +{ + using AutoRegistration.Api.Attributes; + using AutoRegistration.Api.Enumerations; + + [Component(EnLifestyle.Transient)] + internal class DerivedFromUnregisteredService : BaseUnregisteredService + { + } +} \ No newline at end of file diff --git a/tests/Tests/CompositionRoot.Test/AutoRegistrationTest/ExternalResolvable.cs b/tests/Tests/CompositionRoot.Test/AutoRegistrationTest/ExternalResolvable.cs new file mode 100644 index 00000000..93e954a2 --- /dev/null +++ b/tests/Tests/CompositionRoot.Test/AutoRegistrationTest/ExternalResolvable.cs @@ -0,0 +1,17 @@ +namespace SpaceEngineers.Core.CompositionRoot.Test.AutoRegistrationTest +{ + using System; + using AutoRegistration.Api.Abstractions; + using AutoRegistration.Api.Attributes; + using AutoRegistration.Api.Enumerations; + + [Component(EnLifestyle.Transient)] + internal class ExternalResolvable : IProgress, + IResolvable> + { + public void Report(ExternalResolvable value) + { + throw new ArgumentException(nameof(ExternalResolvable), nameof(value)); + } + } +} \ No newline at end of file diff --git a/tests/Tests/CompositionRoot.Test/AutoRegistrationTest/ExternalResolvableOpenGeneric.cs b/tests/Tests/CompositionRoot.Test/AutoRegistrationTest/ExternalResolvableOpenGeneric.cs new file mode 100644 index 00000000..17238730 --- /dev/null +++ b/tests/Tests/CompositionRoot.Test/AutoRegistrationTest/ExternalResolvableOpenGeneric.cs @@ -0,0 +1,18 @@ +namespace SpaceEngineers.Core.CompositionRoot.Test.AutoRegistrationTest +{ + using System; + using AutoRegistration.Api.Abstractions; + using AutoRegistration.Api.Attributes; + using AutoRegistration.Api.Enumerations; + + [Component(EnLifestyle.Transient)] + internal class ExternalResolvableOpenGeneric : IProgress, + IResolvable> + where T : class + { + public void Report(T value) + { + throw new ArgumentException(nameof(ExternalResolvable), nameof(value)); + } + } +} \ No newline at end of file diff --git a/tests/Tests/CompositionRoot.Test/AutoRegistrationTest/ICollectionResolvableTestService.cs b/tests/Tests/CompositionRoot.Test/AutoRegistrationTest/ICollectionResolvableTestService.cs new file mode 100644 index 00000000..f9846a60 --- /dev/null +++ b/tests/Tests/CompositionRoot.Test/AutoRegistrationTest/ICollectionResolvableTestService.cs @@ -0,0 +1,8 @@ +namespace SpaceEngineers.Core.CompositionRoot.Test.AutoRegistrationTest +{ + using AutoRegistration.Api.Abstractions; + + internal interface ICollectionResolvableTestService : ICollectionResolvable + { + } +} \ No newline at end of file diff --git a/tests/Tests/CompositionRoot.Test/AutoRegistrationTest/IDecorableService.cs b/tests/Tests/CompositionRoot.Test/AutoRegistrationTest/IDecorableService.cs new file mode 100644 index 00000000..ca713607 --- /dev/null +++ b/tests/Tests/CompositionRoot.Test/AutoRegistrationTest/IDecorableService.cs @@ -0,0 +1,6 @@ +namespace SpaceEngineers.Core.CompositionRoot.Test.AutoRegistrationTest +{ + internal interface IDecorableService + { + } +} \ No newline at end of file diff --git a/tests/Tests/CompositionRoot.Test/AutoRegistrationTest/IDecorableServiceDecorator.cs b/tests/Tests/CompositionRoot.Test/AutoRegistrationTest/IDecorableServiceDecorator.cs new file mode 100644 index 00000000..8171f56e --- /dev/null +++ b/tests/Tests/CompositionRoot.Test/AutoRegistrationTest/IDecorableServiceDecorator.cs @@ -0,0 +1,8 @@ +namespace SpaceEngineers.Core.CompositionRoot.Test.AutoRegistrationTest +{ + using AutoRegistration.Api.Abstractions; + + internal interface IDecorableServiceDecorator : IDecorator + { + } +} \ No newline at end of file diff --git a/tests/Tests/CompositionRoot.Test/AutoRegistrationTest/IIndependentTestService.cs b/tests/Tests/CompositionRoot.Test/AutoRegistrationTest/IIndependentTestService.cs new file mode 100644 index 00000000..4ba08a0c --- /dev/null +++ b/tests/Tests/CompositionRoot.Test/AutoRegistrationTest/IIndependentTestService.cs @@ -0,0 +1,6 @@ +namespace SpaceEngineers.Core.CompositionRoot.Test.AutoRegistrationTest +{ + internal interface IIndependentTestService + { + } +} \ No newline at end of file diff --git a/tests/Tests/CompositionRoot.Test/AutoRegistrationTest/IManuallyRegisteredService.cs b/tests/Tests/CompositionRoot.Test/AutoRegistrationTest/IManuallyRegisteredService.cs new file mode 100644 index 00000000..4f970ef9 --- /dev/null +++ b/tests/Tests/CompositionRoot.Test/AutoRegistrationTest/IManuallyRegisteredService.cs @@ -0,0 +1,6 @@ +namespace SpaceEngineers.Core.CompositionRoot.Test.AutoRegistrationTest +{ + internal interface IManuallyRegisteredService + { + } +} \ No newline at end of file diff --git a/tests/Tests/CompositionRoot.Test/AutoRegistrationTest/IOpenGenericDecorableService.cs b/tests/Tests/CompositionRoot.Test/AutoRegistrationTest/IOpenGenericDecorableService.cs new file mode 100644 index 00000000..6a667000 --- /dev/null +++ b/tests/Tests/CompositionRoot.Test/AutoRegistrationTest/IOpenGenericDecorableService.cs @@ -0,0 +1,6 @@ +namespace SpaceEngineers.Core.CompositionRoot.Test.AutoRegistrationTest +{ + internal interface IOpenGenericDecorableService + { + } +} \ No newline at end of file diff --git a/tests/Tests/CompositionRoot.Test/AutoRegistrationTest/IOpenGenericDecorableServiceDecorator.cs b/tests/Tests/CompositionRoot.Test/AutoRegistrationTest/IOpenGenericDecorableServiceDecorator.cs new file mode 100644 index 00000000..bb8fece9 --- /dev/null +++ b/tests/Tests/CompositionRoot.Test/AutoRegistrationTest/IOpenGenericDecorableServiceDecorator.cs @@ -0,0 +1,8 @@ +namespace SpaceEngineers.Core.CompositionRoot.Test.AutoRegistrationTest +{ + using AutoRegistration.Api.Abstractions; + + internal interface IOpenGenericDecorableServiceDecorator : IDecorator> + { + } +} \ No newline at end of file diff --git a/tests/Tests/CompositionRoot.Test/AutoRegistrationTest/IOpenGenericTestService.cs b/tests/Tests/CompositionRoot.Test/AutoRegistrationTest/IOpenGenericTestService.cs new file mode 100644 index 00000000..015dcd8d --- /dev/null +++ b/tests/Tests/CompositionRoot.Test/AutoRegistrationTest/IOpenGenericTestService.cs @@ -0,0 +1,6 @@ +namespace SpaceEngineers.Core.CompositionRoot.Test.AutoRegistrationTest +{ + internal interface IOpenGenericTestService + { + } +} \ No newline at end of file diff --git a/tests/Tests/CompositionRoot.Test/AutoRegistrationTest/IScopedCollectionResolvable.cs b/tests/Tests/CompositionRoot.Test/AutoRegistrationTest/IScopedCollectionResolvable.cs new file mode 100644 index 00000000..4a256b30 --- /dev/null +++ b/tests/Tests/CompositionRoot.Test/AutoRegistrationTest/IScopedCollectionResolvable.cs @@ -0,0 +1,6 @@ +namespace SpaceEngineers.Core.CompositionRoot.Test.AutoRegistrationTest +{ + internal interface IScopedCollectionResolvable + { + } +} \ No newline at end of file diff --git a/tests/Tests/CompositionRoot.Test/AutoRegistrationTest/IScopedService.cs b/tests/Tests/CompositionRoot.Test/AutoRegistrationTest/IScopedService.cs new file mode 100644 index 00000000..9c09cbbc --- /dev/null +++ b/tests/Tests/CompositionRoot.Test/AutoRegistrationTest/IScopedService.cs @@ -0,0 +1,9 @@ +namespace SpaceEngineers.Core.CompositionRoot.Test.AutoRegistrationTest +{ + using System.Threading.Tasks; + + internal interface IScopedService + { + Task DoSmth(); + } +} \ No newline at end of file diff --git a/tests/Tests/CompositionRoot.Test/AutoRegistrationTest/ISingletonGenericCollectionResolvableTestService.cs b/tests/Tests/CompositionRoot.Test/AutoRegistrationTest/ISingletonGenericCollectionResolvableTestService.cs new file mode 100644 index 00000000..907cfca6 --- /dev/null +++ b/tests/Tests/CompositionRoot.Test/AutoRegistrationTest/ISingletonGenericCollectionResolvableTestService.cs @@ -0,0 +1,8 @@ +namespace SpaceEngineers.Core.CompositionRoot.Test.AutoRegistrationTest +{ + using AutoRegistration.Api.Abstractions; + + internal interface ISingletonGenericCollectionResolvableTestService : ICollectionResolvable> + { + } +} \ No newline at end of file diff --git a/tests/Tests/CompositionRoot.Test/AutoRegistrationTest/ISingletonService.cs b/tests/Tests/CompositionRoot.Test/AutoRegistrationTest/ISingletonService.cs new file mode 100644 index 00000000..ca0ea3ed --- /dev/null +++ b/tests/Tests/CompositionRoot.Test/AutoRegistrationTest/ISingletonService.cs @@ -0,0 +1,6 @@ +namespace SpaceEngineers.Core.CompositionRoot.Test.AutoRegistrationTest +{ + internal interface ISingletonService + { + } +} \ No newline at end of file diff --git a/tests/Tests/CompositionRoot.Test/AutoRegistrationTest/IUnregisteredService.cs b/tests/Tests/CompositionRoot.Test/AutoRegistrationTest/IUnregisteredService.cs new file mode 100644 index 00000000..94e9be8c --- /dev/null +++ b/tests/Tests/CompositionRoot.Test/AutoRegistrationTest/IUnregisteredService.cs @@ -0,0 +1,6 @@ +namespace SpaceEngineers.Core.CompositionRoot.Test.AutoRegistrationTest +{ + internal interface IUnregisteredService + { + } +} \ No newline at end of file diff --git a/tests/Tests/CompositionRoot.Test/AutoRegistrationTest/IWiredTestService.cs b/tests/Tests/CompositionRoot.Test/AutoRegistrationTest/IWiredTestService.cs new file mode 100644 index 00000000..6887a6ee --- /dev/null +++ b/tests/Tests/CompositionRoot.Test/AutoRegistrationTest/IWiredTestService.cs @@ -0,0 +1,7 @@ +namespace SpaceEngineers.Core.CompositionRoot.Test.AutoRegistrationTest +{ + internal interface IWiredTestService + { + IIndependentTestService IndependentTestService { get; } + } +} \ No newline at end of file diff --git a/tests/Tests/CompositionRoot.Test/AutoRegistrationTest/IWithInjectedDependencyContainer.cs b/tests/Tests/CompositionRoot.Test/AutoRegistrationTest/IWithInjectedDependencyContainer.cs new file mode 100644 index 00000000..9eb2aff5 --- /dev/null +++ b/tests/Tests/CompositionRoot.Test/AutoRegistrationTest/IWithInjectedDependencyContainer.cs @@ -0,0 +1,7 @@ +namespace SpaceEngineers.Core.CompositionRoot.Test.AutoRegistrationTest +{ + internal interface IWithInjectedDependencyContainer + { + IDependencyContainer DependencyContainer { get; } + } +} \ No newline at end of file diff --git a/tests/Tests/CompositionRoot.Test/AutoRegistrationTest/IndependentTestService.cs b/tests/Tests/CompositionRoot.Test/AutoRegistrationTest/IndependentTestService.cs new file mode 100644 index 00000000..700564a8 --- /dev/null +++ b/tests/Tests/CompositionRoot.Test/AutoRegistrationTest/IndependentTestService.cs @@ -0,0 +1,12 @@ +namespace SpaceEngineers.Core.CompositionRoot.Test.AutoRegistrationTest +{ + using AutoRegistration.Api.Abstractions; + using AutoRegistration.Api.Attributes; + using AutoRegistration.Api.Enumerations; + + [Component(EnLifestyle.Singleton)] + internal class IndependentTestService : IIndependentTestService, + IResolvable + { + } +} \ No newline at end of file diff --git a/tests/Tests/CompositionRoot.Test/AutoRegistrationTest/ManuallyRegisteredService.cs b/tests/Tests/CompositionRoot.Test/AutoRegistrationTest/ManuallyRegisteredService.cs new file mode 100644 index 00000000..79a66f5e --- /dev/null +++ b/tests/Tests/CompositionRoot.Test/AutoRegistrationTest/ManuallyRegisteredService.cs @@ -0,0 +1,12 @@ +namespace SpaceEngineers.Core.CompositionRoot.Test.AutoRegistrationTest +{ + using AutoRegistration.Api.Abstractions; + using AutoRegistration.Api.Attributes; + + [ManuallyRegisteredComponent(nameof(DependencyContainerOverridesTest.OverrideManuallyRegisteredComponentTest))] + internal class ManuallyRegisteredService : IManuallyRegisteredService, + IResolvable, + IResolvable + { + } +} \ No newline at end of file diff --git a/tests/Tests/CompositionRoot.Test/AutoRegistrationTest/ManuallyRegisteredServiceOverride.cs b/tests/Tests/CompositionRoot.Test/AutoRegistrationTest/ManuallyRegisteredServiceOverride.cs new file mode 100644 index 00000000..c897576b --- /dev/null +++ b/tests/Tests/CompositionRoot.Test/AutoRegistrationTest/ManuallyRegisteredServiceOverride.cs @@ -0,0 +1,11 @@ +namespace SpaceEngineers.Core.CompositionRoot.Test.AutoRegistrationTest +{ + using AutoRegistration.Api.Abstractions; + using AutoRegistration.Api.Attributes; + + [ComponentOverride] + internal class ManuallyRegisteredServiceOverride : IManuallyRegisteredService, + IResolvable + { + } +} \ No newline at end of file diff --git a/tests/Tests/CompositionRoot.Test/AutoRegistrationTest/OpenGenericDecorableService.cs b/tests/Tests/CompositionRoot.Test/AutoRegistrationTest/OpenGenericDecorableService.cs new file mode 100644 index 00000000..a252227a --- /dev/null +++ b/tests/Tests/CompositionRoot.Test/AutoRegistrationTest/OpenGenericDecorableService.cs @@ -0,0 +1,12 @@ +namespace SpaceEngineers.Core.CompositionRoot.Test.AutoRegistrationTest +{ + using AutoRegistration.Api.Abstractions; + using AutoRegistration.Api.Attributes; + using AutoRegistration.Api.Enumerations; + + [Component(EnLifestyle.Transient)] + internal class OpenGenericDecorableService : IOpenGenericDecorableService, + IResolvable> + { + } +} \ No newline at end of file diff --git a/tests/Tests/CompositionRoot.Test/AutoRegistrationTest/OpenGenericDecorableServiceDecorator1.cs b/tests/Tests/CompositionRoot.Test/AutoRegistrationTest/OpenGenericDecorableServiceDecorator1.cs new file mode 100644 index 00000000..eb51ff32 --- /dev/null +++ b/tests/Tests/CompositionRoot.Test/AutoRegistrationTest/OpenGenericDecorableServiceDecorator1.cs @@ -0,0 +1,19 @@ +namespace SpaceEngineers.Core.CompositionRoot.Test.AutoRegistrationTest +{ + using AutoRegistration.Api.Attributes; + using AutoRegistration.Api.Enumerations; + using Basics.Attributes; + + [Component(EnLifestyle.Transient)] + [After(typeof(OpenGenericDecorableServiceDecorator2<>))] + internal class OpenGenericDecorableServiceDecorator1 : IOpenGenericDecorableService, + IOpenGenericDecorableServiceDecorator + { + public OpenGenericDecorableServiceDecorator1(IOpenGenericDecorableService decorateee) + { + Decoratee = decorateee; + } + + public IOpenGenericDecorableService Decoratee { get; } + } +} \ No newline at end of file diff --git a/tests/Tests/CompositionRoot.Test/AutoRegistrationTest/OpenGenericDecorableServiceDecorator2.cs b/tests/Tests/CompositionRoot.Test/AutoRegistrationTest/OpenGenericDecorableServiceDecorator2.cs new file mode 100644 index 00000000..2585e347 --- /dev/null +++ b/tests/Tests/CompositionRoot.Test/AutoRegistrationTest/OpenGenericDecorableServiceDecorator2.cs @@ -0,0 +1,19 @@ +namespace SpaceEngineers.Core.CompositionRoot.Test.AutoRegistrationTest +{ + using AutoRegistration.Api.Attributes; + using AutoRegistration.Api.Enumerations; + using Basics.Attributes; + + [Component(EnLifestyle.Transient)] + [After(typeof(OpenGenericDecorableServiceDecorator3<>))] + internal class OpenGenericDecorableServiceDecorator2 : IOpenGenericDecorableService, + IOpenGenericDecorableServiceDecorator + { + public OpenGenericDecorableServiceDecorator2(IOpenGenericDecorableService decorateee) + { + Decoratee = decorateee; + } + + public IOpenGenericDecorableService Decoratee { get; } + } +} \ No newline at end of file diff --git a/tests/Tests/CompositionRoot.Test/AutoRegistrationTest/OpenGenericDecorableServiceDecorator3.cs b/tests/Tests/CompositionRoot.Test/AutoRegistrationTest/OpenGenericDecorableServiceDecorator3.cs new file mode 100644 index 00000000..e5741a59 --- /dev/null +++ b/tests/Tests/CompositionRoot.Test/AutoRegistrationTest/OpenGenericDecorableServiceDecorator3.cs @@ -0,0 +1,17 @@ +namespace SpaceEngineers.Core.CompositionRoot.Test.AutoRegistrationTest +{ + using AutoRegistration.Api.Attributes; + using AutoRegistration.Api.Enumerations; + + [Component(EnLifestyle.Transient)] + internal class OpenGenericDecorableServiceDecorator3 : IOpenGenericDecorableService, + IOpenGenericDecorableServiceDecorator + { + public OpenGenericDecorableServiceDecorator3(IOpenGenericDecorableService decorateee) + { + Decoratee = decorateee; + } + + public IOpenGenericDecorableService Decoratee { get; } + } +} \ No newline at end of file diff --git a/tests/Tests/CompositionRoot.Test/AutoRegistrationTest/OpenGenericTestService.cs b/tests/Tests/CompositionRoot.Test/AutoRegistrationTest/OpenGenericTestService.cs new file mode 100644 index 00000000..1bd2f9a6 --- /dev/null +++ b/tests/Tests/CompositionRoot.Test/AutoRegistrationTest/OpenGenericTestService.cs @@ -0,0 +1,12 @@ +namespace SpaceEngineers.Core.CompositionRoot.Test.AutoRegistrationTest +{ + using AutoRegistration.Api.Abstractions; + using AutoRegistration.Api.Attributes; + using AutoRegistration.Api.Enumerations; + + [Component(EnLifestyle.Transient)] + internal class OpenGenericTestService : IOpenGenericTestService, + IResolvable> + { + } +} \ No newline at end of file diff --git a/tests/Tests/CompositionRoot.Test/AutoRegistrationTest/ScopedCollectionResolvable.cs b/tests/Tests/CompositionRoot.Test/AutoRegistrationTest/ScopedCollectionResolvable.cs new file mode 100644 index 00000000..ae549726 --- /dev/null +++ b/tests/Tests/CompositionRoot.Test/AutoRegistrationTest/ScopedCollectionResolvable.cs @@ -0,0 +1,11 @@ +namespace SpaceEngineers.Core.CompositionRoot.Test.AutoRegistrationTest +{ + using AutoRegistration.Api.Abstractions; + using AutoRegistration.Api.Attributes; + using AutoRegistration.Api.Enumerations; + + [Component(EnLifestyle.Scoped)] + internal class ScopedCollectionResolvable : IScopedCollectionResolvable, ICollectionResolvable + { + } +} \ No newline at end of file diff --git a/tests/Tests/CompositionRoot.Test/AutoRegistrationTest/ScopedCollectionResolvableDecorator.cs b/tests/Tests/CompositionRoot.Test/AutoRegistrationTest/ScopedCollectionResolvableDecorator.cs new file mode 100644 index 00000000..535515f7 --- /dev/null +++ b/tests/Tests/CompositionRoot.Test/AutoRegistrationTest/ScopedCollectionResolvableDecorator.cs @@ -0,0 +1,16 @@ +namespace SpaceEngineers.Core.CompositionRoot.Test.AutoRegistrationTest +{ + using AutoRegistration.Api.Abstractions; + using AutoRegistration.Api.Attributes; + + [ManuallyRegisteredComponent(nameof(DependencyContainerOverridesTest.OverrideDecoratorTest))] + internal class ScopedCollectionResolvableDecorator : IScopedCollectionResolvable, IDecorator + { + public ScopedCollectionResolvableDecorator(IScopedCollectionResolvable decoratee) + { + Decoratee = decoratee; + } + + public IScopedCollectionResolvable Decoratee { get; } + } +} \ No newline at end of file diff --git a/tests/Tests/CompositionRoot.Test/AutoRegistrationTest/ScopedCollectionResolvableDecoratorOverride.cs b/tests/Tests/CompositionRoot.Test/AutoRegistrationTest/ScopedCollectionResolvableDecoratorOverride.cs new file mode 100644 index 00000000..e8c40b48 --- /dev/null +++ b/tests/Tests/CompositionRoot.Test/AutoRegistrationTest/ScopedCollectionResolvableDecoratorOverride.cs @@ -0,0 +1,16 @@ +namespace SpaceEngineers.Core.CompositionRoot.Test.AutoRegistrationTest +{ + using AutoRegistration.Api.Abstractions; + using AutoRegistration.Api.Attributes; + + [ComponentOverride] + internal class ScopedCollectionResolvableDecoratorOverride : IScopedCollectionResolvable, IDecorator + { + public ScopedCollectionResolvableDecoratorOverride(IScopedCollectionResolvable decoratee) + { + Decoratee = decoratee; + } + + public IScopedCollectionResolvable Decoratee { get; } + } +} \ No newline at end of file diff --git a/tests/Tests/CompositionRoot.Test/AutoRegistrationTest/ScopedCollectionResolvableDecoratorSingletonOverride.cs b/tests/Tests/CompositionRoot.Test/AutoRegistrationTest/ScopedCollectionResolvableDecoratorSingletonOverride.cs new file mode 100644 index 00000000..bda364d2 --- /dev/null +++ b/tests/Tests/CompositionRoot.Test/AutoRegistrationTest/ScopedCollectionResolvableDecoratorSingletonOverride.cs @@ -0,0 +1,16 @@ +namespace SpaceEngineers.Core.CompositionRoot.Test.AutoRegistrationTest +{ + using AutoRegistration.Api.Abstractions; + using AutoRegistration.Api.Attributes; + + [ComponentOverride] + internal class ScopedCollectionResolvableDecoratorSingletonOverride : IScopedCollectionResolvable, IDecorator + { + public ScopedCollectionResolvableDecoratorSingletonOverride(IScopedCollectionResolvable decoratee) + { + Decoratee = decoratee; + } + + public IScopedCollectionResolvable Decoratee { get; } + } +} \ No newline at end of file diff --git a/tests/Tests/CompositionRoot.Test/AutoRegistrationTest/ScopedCollectionResolvableDecoratorTransientOverride.cs b/tests/Tests/CompositionRoot.Test/AutoRegistrationTest/ScopedCollectionResolvableDecoratorTransientOverride.cs new file mode 100644 index 00000000..f0f474a7 --- /dev/null +++ b/tests/Tests/CompositionRoot.Test/AutoRegistrationTest/ScopedCollectionResolvableDecoratorTransientOverride.cs @@ -0,0 +1,16 @@ +namespace SpaceEngineers.Core.CompositionRoot.Test.AutoRegistrationTest +{ + using AutoRegistration.Api.Abstractions; + using AutoRegistration.Api.Attributes; + + [ComponentOverride] + internal class ScopedCollectionResolvableDecoratorTransientOverride : IScopedCollectionResolvable, IDecorator + { + public ScopedCollectionResolvableDecoratorTransientOverride(IScopedCollectionResolvable decoratee) + { + Decoratee = decoratee; + } + + public IScopedCollectionResolvable Decoratee { get; } + } +} \ No newline at end of file diff --git a/tests/Tests/CompositionRoot.Test/AutoRegistrationTest/ScopedCollectionResolvableOverride.cs b/tests/Tests/CompositionRoot.Test/AutoRegistrationTest/ScopedCollectionResolvableOverride.cs new file mode 100644 index 00000000..6947df12 --- /dev/null +++ b/tests/Tests/CompositionRoot.Test/AutoRegistrationTest/ScopedCollectionResolvableOverride.cs @@ -0,0 +1,10 @@ +namespace SpaceEngineers.Core.CompositionRoot.Test.AutoRegistrationTest +{ + using AutoRegistration.Api.Abstractions; + using AutoRegistration.Api.Attributes; + + [ComponentOverride] + internal class ScopedCollectionResolvableOverride : IScopedCollectionResolvable, ICollectionResolvable + { + } +} \ No newline at end of file diff --git a/tests/Tests/CompositionRoot.Test/AutoRegistrationTest/ScopedCollectionResolvableSingletonOverride.cs b/tests/Tests/CompositionRoot.Test/AutoRegistrationTest/ScopedCollectionResolvableSingletonOverride.cs new file mode 100644 index 00000000..dd64c2be --- /dev/null +++ b/tests/Tests/CompositionRoot.Test/AutoRegistrationTest/ScopedCollectionResolvableSingletonOverride.cs @@ -0,0 +1,10 @@ +namespace SpaceEngineers.Core.CompositionRoot.Test.AutoRegistrationTest +{ + using AutoRegistration.Api.Abstractions; + using AutoRegistration.Api.Attributes; + + [ComponentOverride] + internal class ScopedCollectionResolvableSingletonOverride : IScopedCollectionResolvable, ICollectionResolvable + { + } +} \ No newline at end of file diff --git a/tests/Tests/CompositionRoot.Test/AutoRegistrationTest/ScopedCollectionResolvableTransientOverride.cs b/tests/Tests/CompositionRoot.Test/AutoRegistrationTest/ScopedCollectionResolvableTransientOverride.cs new file mode 100644 index 00000000..e0e8a474 --- /dev/null +++ b/tests/Tests/CompositionRoot.Test/AutoRegistrationTest/ScopedCollectionResolvableTransientOverride.cs @@ -0,0 +1,10 @@ +namespace SpaceEngineers.Core.CompositionRoot.Test.AutoRegistrationTest +{ + using AutoRegistration.Api.Abstractions; + using AutoRegistration.Api.Attributes; + + [ComponentOverride] + internal class ScopedCollectionResolvableTransientOverride : IScopedCollectionResolvable, ICollectionResolvable + { + } +} \ No newline at end of file diff --git a/tests/Tests/CompositionRoot.Test/AutoRegistrationTest/ScopedService.cs b/tests/Tests/CompositionRoot.Test/AutoRegistrationTest/ScopedService.cs new file mode 100644 index 00000000..c980d13f --- /dev/null +++ b/tests/Tests/CompositionRoot.Test/AutoRegistrationTest/ScopedService.cs @@ -0,0 +1,18 @@ +namespace SpaceEngineers.Core.CompositionRoot.Test.AutoRegistrationTest +{ + using System; + using System.Threading.Tasks; + using AutoRegistration.Api.Abstractions; + using AutoRegistration.Api.Attributes; + using AutoRegistration.Api.Enumerations; + + [Component(EnLifestyle.Scoped)] + internal class ScopedService : IScopedService, + IResolvable + { + public Task DoSmth() + { + return Task.Delay(TimeSpan.FromMilliseconds(10)); + } + } +} \ No newline at end of file diff --git a/tests/Tests/CompositionRoot.Test/AutoRegistrationTest/ScopedServiceOverride.cs b/tests/Tests/CompositionRoot.Test/AutoRegistrationTest/ScopedServiceOverride.cs new file mode 100644 index 00000000..afa5ff27 --- /dev/null +++ b/tests/Tests/CompositionRoot.Test/AutoRegistrationTest/ScopedServiceOverride.cs @@ -0,0 +1,17 @@ +namespace SpaceEngineers.Core.CompositionRoot.Test.AutoRegistrationTest +{ + using System; + using System.Threading.Tasks; + using AutoRegistration.Api.Abstractions; + using AutoRegistration.Api.Attributes; + + [ComponentOverride] + internal class ScopedServiceOverride : IScopedService, + IResolvable + { + public Task DoSmth() + { + return Task.Delay(TimeSpan.FromMilliseconds(10)); + } + } +} \ No newline at end of file diff --git a/tests/Tests/CompositionRoot.Test/AutoRegistrationTest/ScopedServiceSingletonOverride.cs b/tests/Tests/CompositionRoot.Test/AutoRegistrationTest/ScopedServiceSingletonOverride.cs new file mode 100644 index 00000000..fb2be7a6 --- /dev/null +++ b/tests/Tests/CompositionRoot.Test/AutoRegistrationTest/ScopedServiceSingletonOverride.cs @@ -0,0 +1,17 @@ +namespace SpaceEngineers.Core.CompositionRoot.Test.AutoRegistrationTest +{ + using System; + using System.Threading.Tasks; + using AutoRegistration.Api.Abstractions; + using AutoRegistration.Api.Attributes; + + [ComponentOverride] + internal class ScopedServiceSingletonOverride : IScopedService, + IResolvable + { + public Task DoSmth() + { + return Task.Delay(TimeSpan.FromMilliseconds(10)); + } + } +} \ No newline at end of file diff --git a/tests/Tests/CompositionRoot.Test/AutoRegistrationTest/ScopedServiceTransientOverride.cs b/tests/Tests/CompositionRoot.Test/AutoRegistrationTest/ScopedServiceTransientOverride.cs new file mode 100644 index 00000000..5bd3c259 --- /dev/null +++ b/tests/Tests/CompositionRoot.Test/AutoRegistrationTest/ScopedServiceTransientOverride.cs @@ -0,0 +1,17 @@ +namespace SpaceEngineers.Core.CompositionRoot.Test.AutoRegistrationTest +{ + using System; + using System.Threading.Tasks; + using AutoRegistration.Api.Abstractions; + using AutoRegistration.Api.Attributes; + + [ComponentOverride] + internal class ScopedServiceTransientOverride : IScopedService, + IResolvable + { + public Task DoSmth() + { + return Task.Delay(TimeSpan.FromMilliseconds(10)); + } + } +} \ No newline at end of file diff --git a/tests/Tests/CompositionRoot.Test/AutoRegistrationTest/SingletonGenericCollectionResolvableTestServiceImpl1.cs b/tests/Tests/CompositionRoot.Test/AutoRegistrationTest/SingletonGenericCollectionResolvableTestServiceImpl1.cs new file mode 100644 index 00000000..42f97a4f --- /dev/null +++ b/tests/Tests/CompositionRoot.Test/AutoRegistrationTest/SingletonGenericCollectionResolvableTestServiceImpl1.cs @@ -0,0 +1,12 @@ +namespace SpaceEngineers.Core.CompositionRoot.Test.AutoRegistrationTest +{ + using AutoRegistration.Api.Attributes; + using AutoRegistration.Api.Enumerations; + using Basics.Attributes; + + [Component(EnLifestyle.Singleton)] + [After(typeof(SingletonGenericCollectionResolvableTestServiceImpl2<>))] + internal class SingletonGenericCollectionResolvableTestServiceImpl1 : ISingletonGenericCollectionResolvableTestService + { + } +} \ No newline at end of file diff --git a/tests/Tests/CompositionRoot.Test/AutoRegistrationTest/SingletonGenericCollectionResolvableTestServiceImpl2.cs b/tests/Tests/CompositionRoot.Test/AutoRegistrationTest/SingletonGenericCollectionResolvableTestServiceImpl2.cs new file mode 100644 index 00000000..3dd168dd --- /dev/null +++ b/tests/Tests/CompositionRoot.Test/AutoRegistrationTest/SingletonGenericCollectionResolvableTestServiceImpl2.cs @@ -0,0 +1,12 @@ +namespace SpaceEngineers.Core.CompositionRoot.Test.AutoRegistrationTest +{ + using AutoRegistration.Api.Attributes; + using AutoRegistration.Api.Enumerations; + using Basics.Attributes; + + [Component(EnLifestyle.Singleton)] + [After(typeof(SingletonGenericCollectionResolvableTestServiceImpl3<>))] + internal class SingletonGenericCollectionResolvableTestServiceImpl2 : ISingletonGenericCollectionResolvableTestService + { + } +} \ No newline at end of file diff --git a/tests/Tests/CompositionRoot.Test/AutoRegistrationTest/SingletonGenericCollectionResolvableTestServiceImpl3.cs b/tests/Tests/CompositionRoot.Test/AutoRegistrationTest/SingletonGenericCollectionResolvableTestServiceImpl3.cs new file mode 100644 index 00000000..a5cc9952 --- /dev/null +++ b/tests/Tests/CompositionRoot.Test/AutoRegistrationTest/SingletonGenericCollectionResolvableTestServiceImpl3.cs @@ -0,0 +1,10 @@ +namespace SpaceEngineers.Core.CompositionRoot.Test.AutoRegistrationTest +{ + using AutoRegistration.Api.Attributes; + using AutoRegistration.Api.Enumerations; + + [Component(EnLifestyle.Singleton)] + internal class SingletonGenericCollectionResolvableTestServiceImpl3 : ISingletonGenericCollectionResolvableTestService + { + } +} \ No newline at end of file diff --git a/tests/Tests/CompositionRoot.Test/AutoRegistrationTest/SingletonService.cs b/tests/Tests/CompositionRoot.Test/AutoRegistrationTest/SingletonService.cs new file mode 100644 index 00000000..5957c12a --- /dev/null +++ b/tests/Tests/CompositionRoot.Test/AutoRegistrationTest/SingletonService.cs @@ -0,0 +1,12 @@ +namespace SpaceEngineers.Core.CompositionRoot.Test.AutoRegistrationTest +{ + using AutoRegistration.Api.Abstractions; + using AutoRegistration.Api.Attributes; + using AutoRegistration.Api.Enumerations; + + [Component(EnLifestyle.Singleton)] + internal class SingletonService : ISingletonService, + IResolvable + { + } +} \ No newline at end of file diff --git a/tests/Tests/CompositionRoot.Test/AutoRegistrationTest/WiredTestService.cs b/tests/Tests/CompositionRoot.Test/AutoRegistrationTest/WiredTestService.cs new file mode 100644 index 00000000..9e8ab96b --- /dev/null +++ b/tests/Tests/CompositionRoot.Test/AutoRegistrationTest/WiredTestService.cs @@ -0,0 +1,18 @@ +namespace SpaceEngineers.Core.CompositionRoot.Test.AutoRegistrationTest +{ + using AutoRegistration.Api.Abstractions; + using AutoRegistration.Api.Attributes; + using AutoRegistration.Api.Enumerations; + + [Component(EnLifestyle.Transient)] + internal class WiredTestService : IWiredTestService, + IResolvable + { + public WiredTestService(IIndependentTestService independentTestService) + { + IndependentTestService = independentTestService; + } + + public IIndependentTestService IndependentTestService { get; } + } +} \ No newline at end of file diff --git a/tests/Tests/CompositionRoot.Test/AutoRegistrationTest/WithInjectedDependencyContainer.cs b/tests/Tests/CompositionRoot.Test/AutoRegistrationTest/WithInjectedDependencyContainer.cs new file mode 100644 index 00000000..8aaf521d --- /dev/null +++ b/tests/Tests/CompositionRoot.Test/AutoRegistrationTest/WithInjectedDependencyContainer.cs @@ -0,0 +1,18 @@ +namespace SpaceEngineers.Core.CompositionRoot.Test.AutoRegistrationTest +{ + using AutoRegistration.Api.Abstractions; + using AutoRegistration.Api.Attributes; + using AutoRegistration.Api.Enumerations; + + [Component(EnLifestyle.Transient)] + internal class WithInjectedDependencyContainer : IWithInjectedDependencyContainer, + IResolvable + { + public WithInjectedDependencyContainer(IDependencyContainer dependencyContainer) + { + DependencyContainer = dependencyContainer; + } + + public IDependencyContainer DependencyContainer { get; } + } +} \ No newline at end of file diff --git a/tests/Tests/CompositionRoot.Test/CompositionRoot.Test.csproj b/tests/Tests/CompositionRoot.Test/CompositionRoot.Test.csproj new file mode 100644 index 00000000..f4c26bc2 --- /dev/null +++ b/tests/Tests/CompositionRoot.Test/CompositionRoot.Test.csproj @@ -0,0 +1,141 @@ + + + + net7.0 + SpaceEngineers.Core.CompositionRoot.Test + SpaceEngineers.Core.CompositionRoot.Test + false + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + IWithInjectedDependencyContainer.cs + + + IWiredTestService.cs + + + ISingletonService.cs + + + ISingletonGenericCollectionResolvableTestService.cs + + + ISingletonGenericCollectionResolvableTestService.cs + + + ISingletonGenericCollectionResolvableTestService.cs + + + ScopedService.cs + + + ScopedService.cs + + + ScopedService.cs + + + IScopedService.cs + + + ScopedCollectionResolvable.cs + + + ScopedCollectionResolvable.cs + + + ScopedCollectionResolvable.cs + + + IScopedCollectionResolvable.cs + + + IScopedCollectionResolvable.cs + + + ScopedCollectionResolvableDecorator.cs + + + ScopedCollectionResolvableDecorator.cs + + + ScopedCollectionResolvableDecorator.cs + + + IUnregisteredService.cs + + + ICollectionResolvableTestService.cs + + + ICollectionResolvableTestService.cs + + + ICollectionResolvableTestService.cs + + + IDecorableService.cs + + + IDecorableServiceDecorator.cs + + + IDecorableServiceDecorator.cs + + + IDecorableServiceDecorator.cs + + + BaseUnregisteredService.cs + + + IIndependentTestService.cs + + + IManuallyRegisteredService.cs + + + IManuallyRegisteredService.cs + + + IOpenGenericDecorableServiceDecorator.cs + + + IOpenGenericDecorableServiceDecorator.cs + + + IOpenGenericDecorableServiceDecorator.cs + + + IOpenGenericDecorableService.cs + + + IOpenGenericTestService.cs + + + IOpenGenericDecorableService.cs + + + \ No newline at end of file diff --git a/tests/Tests/CompositionRoot.Test/DependencyContainerAssemblyLimitationsTest.cs b/tests/Tests/CompositionRoot.Test/DependencyContainerAssemblyLimitationsTest.cs new file mode 100644 index 00000000..f2a88b64 --- /dev/null +++ b/tests/Tests/CompositionRoot.Test/DependencyContainerAssemblyLimitationsTest.cs @@ -0,0 +1,102 @@ +namespace SpaceEngineers.Core.CompositionRoot.Test +{ + using System; + using System.Linq; + using Basics; + using CompositionInfo; + using CompositionRoot; + using Microsoft.Extensions.Configuration; + using SpaceEngineers.Core.Test.Api; + using SpaceEngineers.Core.Test.Api.ClassFixtures; + using Xunit; + using Xunit.Abstractions; + + /// + /// DependencyContainerAssemblyLimitationsTest + /// + public class DependencyContainerAssemblyLimitationsTest : TestBase + { + /// .ctor + /// ITestOutputHelper + /// TestFixture + public DependencyContainerAssemblyLimitationsTest(ITestOutputHelper output, TestFixture fixture) + : base(output, fixture) + { + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + internal void ExactlyBoundedContainerTest(bool mode) + { + var assemblies = new[] + { + AssembliesExtensions.FindRequiredAssembly(AssembliesExtensions.BuildName(nameof(SpaceEngineers), nameof(Core), nameof(CliArgumentsParser))) + }; + + var options = new DependencyContainerOptions() + .WithPluginAssemblies(assemblies); + + var boundedContainer = Fixture.DependencyContainer(options); + + _ = boundedContainer + .Resolve() + .GetCompositionInfo(mode); + + var additionalTypes = new[] + { + typeof(TestAdditionalType), + typeof(IConfigurationProvider), + typeof(ConfigurationProvider) + }; + + options = new DependencyContainerOptions() + .WithPluginAssemblies(assemblies) + .WithAdditionalOurTypes(additionalTypes); + + var compositionInfo = Fixture + .DependencyContainer(options) + .Resolve() + .GetCompositionInfo(mode); + + Output.WriteLine($"Total: {compositionInfo.Count}{Environment.NewLine}"); + Output.WriteLine(boundedContainer.Resolve>().Visualize(compositionInfo)); + + var allowedAssemblies = new[] + { + AssembliesExtensions.FindRequiredAssembly(AssembliesExtensions.BuildName(nameof(SimpleInjector))), + AssembliesExtensions.FindRequiredAssembly(AssembliesExtensions.BuildName(nameof(Newtonsoft), nameof(Newtonsoft.Json))), + AssembliesExtensions.FindRequiredAssembly(AssembliesExtensions.BuildName(nameof(SpaceEngineers), nameof(Core), nameof(Basics))), + AssembliesExtensions.FindRequiredAssembly(AssembliesExtensions.BuildName(nameof(SpaceEngineers), nameof(Core), nameof(AutoRegistration), nameof(AutoRegistration.Api))), + AssembliesExtensions.FindRequiredAssembly(AssembliesExtensions.BuildName(nameof(SpaceEngineers), nameof(Core), nameof(CompositionRoot))), + AssembliesExtensions.FindRequiredAssembly(AssembliesExtensions.BuildName(nameof(SpaceEngineers), nameof(Core), nameof(CliArgumentsParser))), + }; + + Assert.True(compositionInfo.All(Satisfies)); + + bool Satisfies(IDependencyInfo info) + { + return TypeSatisfies(info.ServiceType) + && TypeSatisfies(info.ImplementationType) + && info.Dependencies.All(Satisfies); + } + + bool TypeSatisfies(Type type) + { + var satisfies = allowedAssemblies.Contains(type.Assembly) + || additionalTypes.Contains(type); + + if (!satisfies) + { + Output.WriteLine(type.FullName); + } + + return satisfies; + } + } + + private class TestAdditionalType + { + } + } +} \ No newline at end of file diff --git a/tests/Tests/CompositionRoot.Test/DependencyContainerDecoratorsTest.cs b/tests/Tests/CompositionRoot.Test/DependencyContainerDecoratorsTest.cs new file mode 100644 index 00000000..26063f9f --- /dev/null +++ b/tests/Tests/CompositionRoot.Test/DependencyContainerDecoratorsTest.cs @@ -0,0 +1,92 @@ +namespace SpaceEngineers.Core.CompositionRoot.Test +{ + using System; + using System.Collections.Generic; + using AutoRegistrationTest; + using Basics; + using Registrations; + using SpaceEngineers.Core.Test.Api; + using SpaceEngineers.Core.Test.Api.ClassFixtures; + using Xunit; + using Xunit.Abstractions; + + /// + /// DependencyContainerDecoratorsTest + /// + public class DependencyContainerDecoratorsTest : TestBase + { + /// .ctor + /// ITestOutputHelper + /// TestFixture + public DependencyContainerDecoratorsTest(ITestOutputHelper output, TestFixture fixture) + : base(output, fixture) + { + var assemblies = new[] + { + AssembliesExtensions.FindRequiredAssembly(AssembliesExtensions.BuildName(nameof(SpaceEngineers), nameof(Core), nameof(CompositionRoot), nameof(Test))) + }; + + var options = new DependencyContainerOptions() + .WithPluginAssemblies(assemblies) + .WithManualRegistrations(new ManuallyRegisteredServiceManualRegistration()); + + DependencyContainer = fixture.DependencyContainer(options); + } + + private IDependencyContainer DependencyContainer { get; } + + [Fact] + internal void DecoratorTest() + { + var service = DependencyContainer.Resolve(); + + var types = new Dictionary + { + [typeof(DecorableServiceDecorator1)] = typeof(DecorableServiceDecorator2), + [typeof(DecorableServiceDecorator2)] = typeof(DecorableServiceDecorator3), + [typeof(DecorableServiceDecorator3)] = typeof(DecorableService) + }; + + void CheckRecursive(IDecorableService resolved, Type type) + { + Assert.True(resolved.GetType() == type); + Output.WriteLine(type.Name); + + if (types.TryGetValue(type, out var nextDecorateeType)) + { + var decorator = (IDecorableServiceDecorator)resolved; + CheckRecursive(decorator.Decoratee, nextDecorateeType); + } + } + + CheckRecursive(service, typeof(DecorableServiceDecorator1)); + } + + [Fact] + internal void OpenGenericDecoratorTest() + { + var service = DependencyContainer.Resolve>(); + + var types = new Dictionary + { + [typeof(OpenGenericDecorableServiceDecorator1)] = typeof(OpenGenericDecorableServiceDecorator2), + [typeof(OpenGenericDecorableServiceDecorator2)] = typeof(OpenGenericDecorableServiceDecorator3), + [typeof(OpenGenericDecorableServiceDecorator3)] = typeof(OpenGenericDecorableService) + }; + + void CheckRecursive(IOpenGenericDecorableService resolved, Type type) + { + Assert.True(resolved.GetType() == type); + Output.WriteLine(type.Name); + + if (types.TryGetValue(type, out var nextDecorateeType)) + { + var decorator = (IOpenGenericDecorableServiceDecorator)resolved; + CheckRecursive(decorator.Decoratee, nextDecorateeType); + } + } + + CheckRecursive(service, typeof(OpenGenericDecorableServiceDecorator1)); + } + } +} \ No newline at end of file diff --git a/tests/Tests/CompositionRoot.Test/DependencyContainerDependenciesTest.cs b/tests/Tests/CompositionRoot.Test/DependencyContainerDependenciesTest.cs new file mode 100644 index 00000000..6a31b889 --- /dev/null +++ b/tests/Tests/CompositionRoot.Test/DependencyContainerDependenciesTest.cs @@ -0,0 +1,191 @@ +namespace SpaceEngineers.Core.CompositionRoot.Test +{ + using System; + using System.Collections.Generic; + using System.Linq; + using System.Reflection; + using Basics; + using Registrations; + using SpaceEngineers.Core.Test.Api; + using SpaceEngineers.Core.Test.Api.ClassFixtures; + using Xunit; + using Xunit.Abstractions; + + /// + /// DependencyContainerDependenciesTest + /// + public class DependencyContainerDependenciesTest : TestBase + { + /// .ctor + /// ITestOutputHelper + /// TestFixture + public DependencyContainerDependenciesTest(ITestOutputHelper output, TestFixture fixture) + : base(output, fixture) + { + var assemblies = new[] + { + AssembliesExtensions.FindRequiredAssembly(AssembliesExtensions.BuildName(nameof(SpaceEngineers), nameof(Core), nameof(Core.Test), nameof(Core.Test.Api))), + AssembliesExtensions.FindRequiredAssembly(AssembliesExtensions.BuildName(nameof(SpaceEngineers), nameof(Core), nameof(CompositionRoot), nameof(Test))) + }; + + var options = new DependencyContainerOptions() + .WithPluginAssemblies(assemblies) + .WithManualRegistrations(new ManuallyRegisteredServiceManualRegistration()); + + DependencyContainer = fixture.DependencyContainer(options); + } + + private IDependencyContainer DependencyContainer { get; } + + [Fact] + internal void IsOurReferenceTest() + { + var solutionFile = SolutionExtensions.SolutionFile(); + Output.WriteLine(solutionFile.FullName); + + var directory = solutionFile.Directory + ?? throw new InvalidOperationException("Solution directory wasn't found"); + + var ourAssembliesNames = directory + .ProjectFiles() + .Select(p => p.AssemblyName()) + .ToHashSet(); + + var provider = DependencyContainer.Resolve(); + + var ourAssemblies = provider + .AllLoadedAssemblies + .Where(assembly => ourAssembliesNames.Contains(assembly.GetName().Name!)) + .ToList(); + + Output.WriteLine("Our assemblies:"); + ourAssemblies.Select(assembly => assembly.GetName().Name).Each(Output.WriteLine); + + var missing = ourAssemblies + .Where(assembly => !provider.OurAssemblies.Contains(assembly)) + .ToList(); + + Output.WriteLine("missing #1:"); + missing.Select(assembly => assembly.GetName().Name).Each(Output.WriteLine); + + Assert.Empty(missing); + + missing = ourAssemblies + .Where(assembly => !AssembliesExtensions.AllOurAssembliesFromCurrentDomain().Contains(assembly)) + .ToList(); + + Output.WriteLine("missing #2:"); + missing.Select(assembly => assembly.GetName().Name).Each(Output.WriteLine); + + Assert.Empty(missing); + } + + [Fact] + internal void UniqueAssembliesTest() + { + var provider = DependencyContainer.Resolve(); + + CheckAssemblies(provider.OurAssemblies); + CheckAssemblies(provider.OurTypes.Select(t => t.Assembly).ToList()); + CheckAssemblies(provider.AllLoadedTypes.Select(t => t.Assembly).ToList()); + + void CheckAssemblies(IReadOnlyCollection assemblies) + { + Assert.Equal(assemblies.Distinct().Count(), assemblies.GroupBy(a => a.FullName).Count()); + } + } + + [Fact] + internal void IsOurTypeTest() + { + var excludedAssemblies = new[] + { + nameof(System), + nameof(Microsoft), + "Windows" + }; + + var allAssemblies = AssembliesExtensions + .AllAssembliesFromCurrentDomain() + .Where(assembly => + { + var assemblyName = assembly.GetName().FullName; + return excludedAssemblies.All(excluded => !assemblyName.StartsWith(excluded, StringComparison.OrdinalIgnoreCase)); + }) + .ToArray(); + + var provider = DependencyContainer.Resolve(); + var ourTypes = provider.OurTypes; + + // #1 - ITypeProvider + var wrongOurTypes = ourTypes + .Where(type => !type.FullName?.StartsWith(nameof(SpaceEngineers), StringComparison.Ordinal) ?? true) + .ShowTypes("#1 - ITypeProvider", Output.WriteLine) + .ToArray(); + + Assert.False(wrongOurTypes.Any()); + + // #2 - missing + wrongOurTypes = allAssemblies + .SelectMany(asm => asm.GetTypes()) + .Except(ourTypes) + .Where(type => provider.IsOurType(type)) + .ShowTypes("#2 - missing", Output.WriteLine) + .ToArray(); + + Assert.False(wrongOurTypes.Any()); + + // #3 - missing + var excludedTypes = new[] + { + "<>f", + "<>c", + "d__", + "", + "AutoGeneratedProgram", + "Xunit", + "System.Runtime.CompilerServices", + "Microsoft.CodeAnalysis", + "Coverlet.Core.Instrumentation.Tracker" + }; + + var expectedAssemblies = new[] + { + AssembliesExtensions.FindRequiredAssembly(AssembliesExtensions.BuildName(nameof(SpaceEngineers), nameof(Core), nameof(Basics))), + + AssembliesExtensions.FindRequiredAssembly(AssembliesExtensions.BuildName(nameof(SpaceEngineers), nameof(Core), nameof(AutoRegistration), nameof(AutoRegistration.Api))), + AssembliesExtensions.FindRequiredAssembly(AssembliesExtensions.BuildName(nameof(SpaceEngineers), nameof(Core), nameof(CompositionRoot))), + + AssembliesExtensions.FindRequiredAssembly(AssembliesExtensions.BuildName(nameof(SpaceEngineers), nameof(Core), nameof(Core.Test), nameof(Core.Test.Api))), + AssembliesExtensions.FindRequiredAssembly(AssembliesExtensions.BuildName(nameof(SpaceEngineers), nameof(Core), nameof(CompositionRoot), nameof(Test))), + + AssembliesExtensions.FindRequiredAssembly("System.Private.CoreLib") + }; + + wrongOurTypes = allAssemblies + .SelectMany(asm => asm.GetTypes()) + .Where(type => (type.FullName?.StartsWith(nameof(SpaceEngineers), StringComparison.Ordinal) ?? true) + && expectedAssemblies.Contains(type.Assembly) + && !provider.IsOurType(type) + && excludedTypes.All(mask => !type.FullName.Contains(mask, StringComparison.Ordinal))) + .ShowTypes("#3 - missing", Output.WriteLine) + .ToArray(); + + Assert.False(wrongOurTypes.Any()); + + // #4 - uniqueness + var notUniqueTypes = ourTypes + .GroupBy(type => type) + .Where(grp => grp.Count() > 1) + .Select(grp => grp.Key.FullName) + .ToList(); + + if (notUniqueTypes.Any()) + { + Output.WriteLine(notUniqueTypes.ToString(Environment.NewLine)); + } + + Assert.Equal(ourTypes.Count, ourTypes.Distinct().Count()); + } + } +} \ No newline at end of file diff --git a/tests/Tests/CompositionRoot.Test/DependencyContainerOverridesTest.cs b/tests/Tests/CompositionRoot.Test/DependencyContainerOverridesTest.cs new file mode 100644 index 00000000..36f47ca8 --- /dev/null +++ b/tests/Tests/CompositionRoot.Test/DependencyContainerOverridesTest.cs @@ -0,0 +1,417 @@ +namespace SpaceEngineers.Core.CompositionRoot.Test +{ + using System; + using System.Linq; + using AutoRegistration.Api.Abstractions; + using AutoRegistration.Api.Enumerations; + using AutoRegistrationTest; + using CompositionRoot; + using Exceptions; + using Registrations; + using SpaceEngineers.Core.Test.Api; + using SpaceEngineers.Core.Test.Api.ClassFixtures; + using Xunit; + using Xunit.Abstractions; + + /// + /// DependencyContainerOverridesTest + /// + public class DependencyContainerOverridesTest : TestBase + { + /// .ctor + /// ITestOutputHelper + /// TestFixture + public DependencyContainerOverridesTest(ITestOutputHelper output, TestFixture fixture) + : base(output, fixture) + { + } + + [Fact] + internal void OverrideManuallyRegisteredComponentTest() + { + var additionalOurTypes = new[] + { + typeof(IScopedService), + typeof(ScopedService), + typeof(IManuallyRegisteredService), + typeof(ManuallyRegisteredService), + typeof(ManuallyRegisteredServiceOverride) + }; + + // register & override + { + var options = OverrideManuallyRegistered(Fixture) + .WithManualRegistrations(new ManuallyRegisteredServiceManualRegistration()) + .WithAdditionalOurTypes(additionalOurTypes); + + var container = Fixture.DependencyContainer(options); + + Assert.Equal(typeof(ManuallyRegisteredServiceOverride), container.Resolve().GetType()); + Assert.Equal(typeof(ManuallyRegisteredService), container.Resolve().GetType()); + Assert.Equal(typeof(ManuallyRegisteredServiceOverride), container.Resolve().GetType()); + } + + // override without original registration + { + var options = OverrideManuallyRegistered(Fixture); + Assert.Throws(() => Fixture.DependencyContainer(options)); + } + + static DependencyContainerOptions OverrideManuallyRegistered(TestFixture fixture) + { + var registration = fixture.DelegateRegistration(container => + { + container.Register(EnLifestyle.Transient); + }); + + var overrides = fixture.DelegateOverride(container => + { + container.Override(EnLifestyle.Transient); + }); + + return new DependencyContainerOptions() + .WithManualRegistrations(registration) + .WithOverrides(overrides); + } + } + + [Fact] + internal void OverrideResolvableTest() + { + var additionalOurTypes = new[] + { + typeof(IScopedService), + typeof(ScopedService), + typeof(ScopedServiceOverride), + typeof(ScopedServiceSingletonOverride), + typeof(ScopedServiceTransientOverride) + }; + + var resolve = Resolve(Fixture); + + Assert.Equal(typeof(ScopedServiceOverride), resolve(OverrideResolvable(Fixture, additionalOurTypes, EnLifestyle.Scoped)).GetType()); + Assert.Equal(typeof(ScopedServiceSingletonOverride), resolve(OverrideResolvable(Fixture, additionalOurTypes, EnLifestyle.Singleton)).GetType()); + Assert.Throws(() => resolve(OverrideResolvable(Fixture, additionalOurTypes, EnLifestyle.Transient)).GetType()); + + static DependencyContainerOptions OverrideResolvable( + TestFixture fixture, + Type[] additionalOurTypes, + EnLifestyle lifestyle) + where TOverride : IScopedService + { + var overrides = fixture.DelegateOverride(container => + { + container.Override(lifestyle); + }); + + return new DependencyContainerOptions() + .WithOverrides(overrides) + .WithAdditionalOurTypes(additionalOurTypes); + } + + static Func Resolve(TestFixture fixture) + { + return options => + { + var dependencyContainer = fixture.DependencyContainer(options); + + using (dependencyContainer.OpenScope()) + { + return dependencyContainer.Resolve(); + } + }; + } + } + + [Fact] + internal void OverrideCollectionResolvableTest() + { + var additionalOurTypes = new[] + { + typeof(IScopedCollectionResolvable), + typeof(ScopedCollectionResolvable), + typeof(ScopedCollectionResolvableOverride), + typeof(ScopedCollectionResolvableSingletonOverride), + typeof(ScopedCollectionResolvableTransientOverride) + }; + + var resolve = Resolve(Fixture); + + Assert.Equal(typeof(ScopedCollectionResolvableOverride), resolve(OverrideCollectionResolvable(Fixture, additionalOurTypes, EnLifestyle.Scoped)).GetType()); + Assert.Equal(typeof(ScopedCollectionResolvableSingletonOverride), resolve(OverrideCollectionResolvable(Fixture, additionalOurTypes, EnLifestyle.Singleton)).GetType()); + Assert.Equal(typeof(ScopedCollectionResolvableTransientOverride), resolve(OverrideCollectionResolvable(Fixture, additionalOurTypes, EnLifestyle.Transient)).GetType()); + + static DependencyContainerOptions OverrideCollectionResolvable( + TestFixture fixture, + Type[] additionalOurTypes, + EnLifestyle lifestyle) + where TOverride : IScopedCollectionResolvable + { + var overrides = fixture.DelegateOverride(container => + { + container.OverrideCollection( + Enumerable.Empty(), + new[] { (typeof(TOverride), lifestyle) }, + Enumerable.Empty<(Func, EnLifestyle)>()); + }); + + return new DependencyContainerOptions() + .WithOverrides(overrides) + .WithAdditionalOurTypes(additionalOurTypes); + } + + static Func Resolve(TestFixture fixture) + { + return options => + { + var dependencyContainer = fixture.DependencyContainer(options); + + using (dependencyContainer.OpenScope()) + { + return dependencyContainer + .ResolveCollection() + .Single(); + } + }; + } + } + + [Fact] + internal void OverrideDecoratorTest() + { + var additionalOurTypes = new[] + { + typeof(IScopedCollectionResolvable), + typeof(ScopedCollectionResolvable), + typeof(ScopedCollectionResolvableDecorator) + }; + + var resolve = Resolve(Fixture); + + // 1 - override decoratee + var instance = resolve(OverrideDecoratee(Fixture, additionalOurTypes, EnLifestyle.Scoped)); + Assert.Equal(typeof(ScopedCollectionResolvableDecorator), instance.GetType()); + Assert.Equal(typeof(ScopedCollectionResolvableOverride), Decoratee(instance).GetType()); + + instance = resolve(OverrideDecoratee(Fixture, additionalOurTypes, EnLifestyle.Singleton)); + Assert.Equal(typeof(ScopedCollectionResolvableDecorator), instance.GetType()); + Assert.Equal(typeof(ScopedCollectionResolvableSingletonOverride), Decoratee(instance).GetType()); + + Assert.Throws(() => resolve(OverrideDecoratee(Fixture, additionalOurTypes, EnLifestyle.Transient))); + + // 2 - override decorator + instance = resolve(OverrideDecorator(Fixture, additionalOurTypes, EnLifestyle.Scoped)); + Assert.Equal(typeof(ScopedCollectionResolvableDecoratorOverride), instance.GetType()); + Assert.Equal(typeof(ScopedCollectionResolvable), Decoratee(instance).GetType()); + + Assert.Throws(() => instance = resolve(OverrideDecorator(Fixture, additionalOurTypes, EnLifestyle.Singleton))); + + instance = resolve(OverrideDecorator(Fixture, additionalOurTypes, EnLifestyle.Transient)); + Assert.Equal(typeof(ScopedCollectionResolvableDecoratorTransientOverride), instance.GetType()); + Assert.Equal(typeof(ScopedCollectionResolvable), Decoratee(instance).GetType()); + + static DependencyContainerOptions OverrideDecoratee( + TestFixture fixture, + Type[] additionalOurTypes, + EnLifestyle lifestyle) + where TOverride : IScopedCollectionResolvable + { + var registrations = fixture.DelegateRegistration(container => + { + container.RegisterDecorator(EnLifestyle.Scoped); + }); + + var overrides = fixture.DelegateOverride(container => + { + container.OverrideCollection( + Enumerable.Empty(), + new[] { (typeof(TOverride), lifestyle) }, + Enumerable.Empty<(Func, EnLifestyle)>()); + }); + + return new DependencyContainerOptions() + .WithManualRegistrations(registrations) + .WithOverrides(overrides) + .WithAdditionalOurTypes(additionalOurTypes); + } + + static DependencyContainerOptions OverrideDecorator( + TestFixture fixture, + Type[] additionalOurTypes, + EnLifestyle lifestyle) + where TOverride : IScopedCollectionResolvable, IDecorator + { + var registrations = fixture.DelegateRegistration(container => + { + container.RegisterDecorator(EnLifestyle.Scoped); + }); + + var overrides = fixture.DelegateOverride(container => + { + container.Override(lifestyle); + }); + + return new DependencyContainerOptions() + .WithManualRegistrations(registrations) + .WithOverrides(overrides) + .WithAdditionalOurTypes(additionalOurTypes); + } + + static Func Resolve(TestFixture fixture) + { + return options => + { + var dependencyContainer = fixture.DependencyContainer(options); + + using (dependencyContainer.OpenScope()) + { + return dependencyContainer + .ResolveCollection() + .Single(); + } + }; + } + + static T Decoratee(T service) + where T : IScopedCollectionResolvable + { + return service is IDecorator decorator + ? decorator.Decoratee + : service; + } + } + + [Fact] + internal void OverrideInstanceTest() + { + var additionalOurTypes = new[] + { + typeof(IScopedService), + typeof(ScopedService), + typeof(IManuallyRegisteredService), + typeof(ManuallyRegisteredService), + typeof(ManuallyRegisteredServiceOverride) + }; + + var original = new ManuallyRegisteredService(); + + var registration = Fixture.DelegateRegistration(container => + { + container.RegisterInstance(original); + container.RegisterInstance(original); + }); + + var replacement = new ManuallyRegisteredServiceOverride(); + + var overrides = Fixture.DelegateOverride(container => + { + container.OverrideInstance(replacement); + }); + + var options = new DependencyContainerOptions() + .WithManualRegistrations(registration) + .WithOverrides(overrides) + .WithAdditionalOurTypes(additionalOurTypes); + + var dependencyContainer = Fixture.DependencyContainer(options); + var resolved = dependencyContainer.Resolve(); + + Assert.NotSame(original, replacement); + Assert.NotSame(original, resolved); + Assert.Same(replacement, resolved); + Assert.Same(resolved, dependencyContainer.Resolve()); + } + + [Fact] + internal void OverrideInstanceByInstanceProducerTest() + { + var additionalOurTypes = new[] + { + typeof(IScopedService), + typeof(ScopedService), + typeof(IManuallyRegisteredService), + typeof(ManuallyRegisteredService), + typeof(ManuallyRegisteredServiceOverride) + }; + + var original = new ManuallyRegisteredService(); + + var registration = Fixture.DelegateRegistration(container => + { + container.RegisterInstance(original); + container.RegisterInstance(original); + }); + + var replacement = new ManuallyRegisteredServiceOverride(); + + var wrongOverrides = Fixture.DelegateOverride(container => + { + container.OverrideDelegate(() => replacement, EnLifestyle.Scoped); + }); + + var wrongOptions = new DependencyContainerOptions() + .WithManualRegistrations(registration) + .WithOverrides(wrongOverrides) + .WithAdditionalOurTypes(additionalOurTypes); + + Assert.Throws(() => Fixture.DependencyContainer(wrongOptions)); + + var overrides = Fixture.DelegateOverride(container => + { + container.OverrideDelegate(() => replacement, EnLifestyle.Singleton); + }); + + var options = new DependencyContainerOptions() + .WithManualRegistrations(registration) + .WithOverrides(overrides) + .WithAdditionalOurTypes(additionalOurTypes); + + var dependencyContainer = Fixture.DependencyContainer(options); + var resolved = dependencyContainer.Resolve(); + + Assert.NotSame(original, replacement); + Assert.NotSame(original, resolved); + Assert.Same(replacement, resolved); + Assert.Same(resolved, dependencyContainer.Resolve()); + } + + [Fact] + internal void OverrideDelegateTest() + { + var additionalOurTypes = new[] + { + typeof(IScopedService), + typeof(ScopedService), + typeof(IManuallyRegisteredService), + typeof(ManuallyRegisteredService), + typeof(ManuallyRegisteredServiceOverride) + }; + + var originalFactory = new Func(() => new ManuallyRegisteredService()); + + var registration = Fixture.DelegateRegistration(container => + { + container.Advanced.RegisterDelegate(originalFactory, EnLifestyle.Singleton); + container.Advanced.RegisterDelegate(originalFactory, EnLifestyle.Singleton); + }); + + var replacementFactory = new Func(() => new ManuallyRegisteredServiceOverride()); + + var overrides = Fixture.DelegateOverride(container => + { + container.OverrideDelegate(replacementFactory, EnLifestyle.Singleton); + }); + + var options = new DependencyContainerOptions() + .WithManualRegistrations(registration) + .WithOverrides(overrides) + .WithAdditionalOurTypes(additionalOurTypes); + + var dependencyContainer = Fixture.DependencyContainer(options); + var resolved = dependencyContainer.Resolve(); + + Assert.Equal(typeof(ManuallyRegisteredServiceOverride), resolved.GetType()); + Assert.Same(resolved, dependencyContainer.Resolve()); + } + } +} \ No newline at end of file diff --git a/tests/Tests/CompositionRoot.Test/DependencyContainerResolveTest.cs b/tests/Tests/CompositionRoot.Test/DependencyContainerResolveTest.cs new file mode 100644 index 00000000..1aecd4ee --- /dev/null +++ b/tests/Tests/CompositionRoot.Test/DependencyContainerResolveTest.cs @@ -0,0 +1,353 @@ +namespace SpaceEngineers.Core.CompositionRoot.Test +{ + using System; + using System.Collections.Generic; + using System.Linq; + using AutoRegistration.Api.Abstractions; + using AutoRegistration.Api.Attributes; + using AutoRegistration.Api.Enumerations; + using AutoRegistrationTest; + using Basics; + using CompositionRoot; + using Exceptions; + using Registration; + using Registrations; + using SpaceEngineers.Core.Test.Api; + using SpaceEngineers.Core.Test.Api.ClassFixtures; + using Xunit; + using Xunit.Abstractions; + + /// + /// DependencyContainerResolveTest + /// + public class DependencyContainerResolveTest : TestBase + { + /// .ctor + /// ITestOutputHelper + /// TestFixture + public DependencyContainerResolveTest(ITestOutputHelper output, TestFixture fixture) + : base(output, fixture) + { + var assemblies = new[] + { + AssembliesExtensions.FindRequiredAssembly(AssembliesExtensions.BuildName(nameof(SpaceEngineers), nameof(Core), nameof(CompositionRoot), nameof(Test))) + }; + + var options = new DependencyContainerOptions() + .WithPluginAssemblies(assemblies) + .WithManualRegistrations(new ManuallyRegisteredServiceManualRegistration()) + .WithManualRegistrations(fixture.DelegateRegistration(container => + { + container.RegisterInstance(new ConcreteImplementationGenericService()); + })); + + DependencyContainer = fixture.DependencyContainer(options); + } + + private IDependencyContainer DependencyContainer { get; } + + /// + /// ResolveRegisteredServicesTest Data + /// + /// Test data + public static IEnumerable ResolveRegisteredServicesTestData() + { + return new List + { + new object[] + { + typeof(IResolvable<>), + new Func, IEnumerable>(SingleResolvable), + new Func((container, service) => container.Resolve(service)) + }, + new object[] + { + typeof(ICollectionResolvable<>), + new Func, IEnumerable>(Collections), + new Func((container, service) => container.ResolveCollection(service)) + } + }; + + IEnumerable SingleResolvable(IEnumerable source) + { + return source + .Where(t => t.IsSubclassOfOpenGeneric(typeof(IResolvable<>))) + .SelectMany(t => t.ExtractGenericArgumentsAt(typeof(IResolvable<>))) + .Where(type => !type.IsGenericParameter && type.HasAttribute()) + .Select(type => type.GenericTypeDefinitionOrSelf()) + .Distinct(); + } + + IEnumerable Collections(IEnumerable source) + { + return source + .Where(t => t.IsSubclassOfOpenGeneric(typeof(ICollectionResolvable<>))) + .SelectMany(t => t.ExtractGenericArgumentsAt(typeof(ICollectionResolvable<>))) + .Where(type => !type.IsGenericParameter && type.HasAttribute()) + .Select(type => type.GenericTypeDefinitionOrSelf()) + .Distinct(); + } + } + + [Theory] + [MemberData(nameof(ResolveRegisteredServicesTestData))] + internal void ResolveRegisteredServicesTest(Type apiService, + Func, IEnumerable> selector, + Func resolve) + { + Output.WriteLine(apiService.FullName); + + var genericTypeProvider = DependencyContainer.Resolve(); + + using (DependencyContainer.OpenScope()) + { + selector(DependencyContainer.Resolve().OurTypes) + .Each(type => + { + Output.WriteLine(type.FullName); + + var service = !type.IsConstructedOrNonGenericType() + ? genericTypeProvider.CloseByConstraints(type, HybridTypeArgumentSelector(DependencyContainer)) + : type; + + Output.WriteLine(service.FullName); + + if (service.IsSubclassOfOpenGeneric(typeof(IInitializable<>))) + { + Assert.Throws(() => resolve(DependencyContainer, service)); + } + else if (!type.HasAttribute()) + { + resolve(DependencyContainer, service); + } + else + { + Assert.Throws(() => resolve(DependencyContainer, service)); + } + }); + } + } + + [Fact] + internal void DependencyContainerSelfResolveTest() + { + var container = DependencyContainer.Resolve(); + + Assert.True(ReferenceEquals(container, DependencyContainer)); + Assert.True(container.Equals(DependencyContainer)); + + container = DependencyContainer.Resolve().DependencyContainer; + + Assert.True(ReferenceEquals(container, DependencyContainer)); + Assert.True(container.Equals(DependencyContainer)); + } + + [Fact] + internal void SingletonTest() + { + // 1 - resolve via service type + Assert.Equal(DependencyContainer.Resolve(), + DependencyContainer.Resolve()); + + // 2 - resolve via concrete type + Assert.Throws(() => DependencyContainer.Resolve()); + } + + [Fact] + internal void TypedUntypedSingletonTest() + { + Assert.Equal(DependencyContainer.Resolve(), + DependencyContainer.Resolve(typeof(ISingletonService))); + } + + [Fact] + internal void OpenGenericTest() + { + Assert.Equal(typeof(OpenGenericTestService), DependencyContainer.Resolve>().GetType()); + Assert.Equal(typeof(ClosedGenericImplementationOfOpenGenericService), DependencyContainer.Resolve>().GetType()); + } + + [Fact] + internal void AutoWiringSingletonTest() + { + Assert.Equal(DependencyContainer.Resolve().IndependentTestService, + DependencyContainer.Resolve()); + } + + [Fact] + internal void OrderedCollectionResolvableTest() + { + var resolvedTypes = DependencyContainer.ResolveCollection() + .Select(z => z.GetType()); + + var types = new[] + { + typeof(CollectionResolvableTestServiceImpl3), + typeof(CollectionResolvableTestServiceImpl2), + typeof(CollectionResolvableTestServiceImpl1) + }; + + Assert.True(resolvedTypes.SequenceEqual(types)); + } + + [Fact] + internal void UntypedOrderedCollectionResolvableTest() + { + var resolvedTypes = DependencyContainer.ResolveCollection(typeof(ICollectionResolvableTestService)) + .Select(z => z.GetType()); + + var types = new[] + { + typeof(CollectionResolvableTestServiceImpl3), + typeof(CollectionResolvableTestServiceImpl2), + typeof(CollectionResolvableTestServiceImpl1) + }; + + Assert.True(resolvedTypes.SequenceEqual(types)); + } + + [Fact] + internal void SingletonOpenGenericCollectionResolvableTest() + { + var resolvedTypes = DependencyContainer.ResolveCollection>() + .Select(z => z.GetType()); + + var types = new[] + { + typeof(SingletonGenericCollectionResolvableTestServiceImpl3), + typeof(SingletonGenericCollectionResolvableTestServiceImpl2), + typeof(SingletonGenericCollectionResolvableTestServiceImpl1) + }; + + Assert.True(resolvedTypes.SequenceEqual(types)); + + Assert.True(DependencyContainer + .ResolveCollection>() + .SequenceEqual(DependencyContainer.ResolveCollection>())); + } + + [Fact] + internal void ImplementationResolvableTest() + { + Assert.NotNull(DependencyContainer.Resolve()); + + var withDependency = DependencyContainer.Resolve(); + Assert.NotNull(withDependency); + Assert.NotNull(withDependency.Dependency); + + Assert.NotNull(DependencyContainer.Resolve>()); + } + + [Fact] + internal void ResolvableInstanceTest() + { + Assert.NotNull(DependencyContainer.Resolve>()); + } + + [Fact] + internal void ExternalResolvableTest() + { + Assert.Equal(typeof(ExternalResolvable), DependencyContainer.Resolve>().GetType()); + Assert.Equal(typeof(ExternalResolvableOpenGeneric), DependencyContainer.Resolve>().GetType()); + } + + [Fact] + internal void UnregisteredServiceResolveTest() + { + Assert.True(typeof(BaseUnregisteredService).HasAttribute()); + + Assert.True(typeof(DerivedFromUnregisteredService).IsSubclassOf(typeof(BaseUnregisteredService))); + Assert.True(typeof(IUnregisteredService).IsAssignableFrom(typeof(DerivedFromUnregisteredService))); + Assert.True(typeof(IUnregisteredService).IsAssignableFrom(typeof(BaseUnregisteredService))); + + Assert.Equal(typeof(DerivedFromUnregisteredService), DependencyContainer.Resolve().GetType()); + } + + [Fact] + internal void ManualRegistrationResolutionTest() + { + var cctorResolutionBehavior = DependencyContainer.Resolve(); + + Assert.True(cctorResolutionBehavior.TryGetConstructor(typeof(WiredTestService), out var cctor)); + + var parameterType = cctor.GetParameters().Single().ParameterType; + + Assert.Equal(typeof(IIndependentTestService), parameterType); + + var expectedCollection = new[] + { + typeof(CollectionResolvableTestServiceImpl1), + typeof(CollectionResolvableTestServiceImpl2) + }; + + var registration = Fixture.DelegateRegistration(container => + { + container + .Register(EnLifestyle.Transient) + .Register(EnLifestyle.Transient) + .Register(EnLifestyle.Singleton) + .Register(EnLifestyle.Singleton) + .Register(EnLifestyle.Transient) + .Register(EnLifestyle.Singleton) + .RegisterCollectionEntry(EnLifestyle.Transient) + .RegisterCollectionEntry(EnLifestyle.Transient) + .Register, OpenGenericTestService>(EnLifestyle.Transient) + .Register, OpenGenericTestService>(EnLifestyle.Transient); + }); + + var options = new DependencyContainerOptions() + .WithManualRegistrations(registration); + + var localContainer = Fixture.DependencyContainer(options); + + localContainer.Resolve(); + localContainer.Resolve(); + + localContainer.Resolve(); + localContainer.Resolve(); + + localContainer.Resolve(); + + localContainer.Resolve(); + + var actual = localContainer + .ResolveCollection() + .Select(r => r.GetType()) + .ToList(); + + Assert.True(expectedCollection.OrderByDependencies().SequenceEqual(expectedCollection.Reverse())); + Assert.True(expectedCollection.OrderByDependencies().SequenceEqual(actual)); + + localContainer.Resolve>(); + localContainer.Resolve>(); + Assert.Throws(() => localContainer.Resolve>()); + } + + private static Func HybridTypeArgumentSelector(IDependencyContainer dependencyContainer) + { + return ctx => + FromBypassedTypes(ctx) + ?? FromExistedClosedTypesTypeArgumentSelector(dependencyContainer.Resolve().AllLoadedTypes, ctx) + ?? FromMatchesTypeArgumentSelector(ctx); + } + + private static Type? FromBypassedTypes(TypeArgumentSelectionContext ctx) + { + return default; + } + + private static Type? FromExistedClosedTypesTypeArgumentSelector(IEnumerable source, TypeArgumentSelectionContext ctx) + => source + .OrderBy(t => t.IsGenericType) + .FirstOrDefault(t => t.IsConstructedOrNonGenericType() && t.IsSubclassOfOpenGeneric(ctx.OpenGeneric)) + ?.ExtractGenericArgumentsAt(ctx.OpenGeneric, ctx.TypeArgument.GenericParameterPosition) + .FirstOrDefault(); + + private static Type? FromMatchesTypeArgumentSelector(TypeArgumentSelectionContext ctx) + { + return ctx.Matches.Contains(typeof(object)) + ? typeof(object) + : ctx.Matches.OrderBy(t => t.IsGenericType).FirstOrDefault(); + } + } +} \ No newline at end of file diff --git a/tests/Tests/CompositionRoot.Test/Properties/AssemblyInfo.cs b/tests/Tests/CompositionRoot.Test/Properties/AssemblyInfo.cs new file mode 100644 index 00000000..31adb06d --- /dev/null +++ b/tests/Tests/CompositionRoot.Test/Properties/AssemblyInfo.cs @@ -0,0 +1,5 @@ +using System.Reflection; + +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] +[assembly: AssemblyInformationalVersion("1.0.0.0")] \ No newline at end of file diff --git a/tests/Tests/CompositionRoot.Test/Registrations/ManuallyRegisteredServiceManualRegistration.cs b/tests/Tests/CompositionRoot.Test/Registrations/ManuallyRegisteredServiceManualRegistration.cs new file mode 100644 index 00000000..acff320e --- /dev/null +++ b/tests/Tests/CompositionRoot.Test/Registrations/ManuallyRegisteredServiceManualRegistration.cs @@ -0,0 +1,15 @@ +namespace SpaceEngineers.Core.CompositionRoot.Test.Registrations +{ + using AutoRegistration.Api.Enumerations; + using AutoRegistrationTest; + using Registration; + + internal class ManuallyRegisteredServiceManualRegistration : IManualRegistration + { + public void Register(IManualRegistrationsContainer container) + { + container.Register(EnLifestyle.Transient); + container.Register(EnLifestyle.Transient); + } + } +} \ No newline at end of file diff --git a/tests/Tests/CompositionRoot.Test/ScopedContainerTest.cs b/tests/Tests/CompositionRoot.Test/ScopedContainerTest.cs new file mode 100644 index 00000000..6a8d82b3 --- /dev/null +++ b/tests/Tests/CompositionRoot.Test/ScopedContainerTest.cs @@ -0,0 +1,65 @@ +namespace SpaceEngineers.Core.CompositionRoot.Test +{ + using System.Threading.Tasks; + using AutoRegistrationTest; + using Basics; + using Exceptions; + using Registrations; + using SpaceEngineers.Core.Test.Api; + using SpaceEngineers.Core.Test.Api.ClassFixtures; + using Xunit; + using Xunit.Abstractions; + + /// + /// IScopedContainer class tests + /// + public class ScopedContainerTest : TestBase + { + /// .ctor + /// ITestOutputHelper + /// TestFixture + public ScopedContainerTest(ITestOutputHelper output, TestFixture fixture) + : base(output, fixture) + { + var assemblies = new[] + { + AssembliesExtensions.FindRequiredAssembly(AssembliesExtensions.BuildName(nameof(SpaceEngineers), nameof(Core), nameof(CompositionRoot), nameof(Test))) + }; + + var options = new DependencyContainerOptions() + .WithPluginAssemblies(assemblies) + .WithManualRegistrations(new ManuallyRegisteredServiceManualRegistration()); + + DependencyContainer = fixture.DependencyContainer(options); + } + + private IDependencyContainer DependencyContainer { get; } + + [Fact] + internal async Task AsyncScopeTest() + { + Assert.Throws(() => DependencyContainer.Resolve()); + + using (DependencyContainer.OpenScope()) + { + var service = DependencyContainer.Resolve(); + await service.DoSmth().ConfigureAwait(false); + + var anotherService = DependencyContainer.Resolve(); + await anotherService.DoSmth().ConfigureAwait(false); + Assert.True(ReferenceEquals(service, anotherService)); + + using (DependencyContainer.OpenScope()) + { + anotherService = DependencyContainer.Resolve(); + await anotherService.DoSmth().ConfigureAwait(false); + Assert.False(ReferenceEquals(service, anotherService)); + } + + anotherService = DependencyContainer.Resolve(); + await anotherService.DoSmth().ConfigureAwait(false); + Assert.True(ReferenceEquals(service, anotherService)); + } + } + } +} \ No newline at end of file diff --git a/tests/Tests/GenericHost.Test/AuthTest.cs b/tests/Tests/GenericHost.Test/AuthTest.cs new file mode 100644 index 00000000..d3619678 --- /dev/null +++ b/tests/Tests/GenericHost.Test/AuthTest.cs @@ -0,0 +1,73 @@ +namespace SpaceEngineers.Core.GenericHost.Test +{ + using System; + using System.Diagnostics.CodeAnalysis; + using System.IdentityModel.Tokens.Jwt; + using System.Linq; + using Basics; + using GenericEndpoint.Authorization.Host; + using GenericEndpoint.Authorization.Web; + using JwtAuthentication; + using Microsoft.Extensions.Configuration; + using SpaceEngineers.Core.Test.Api; + using SpaceEngineers.Core.Test.Api.ClassFixtures; + using Xunit; + using Xunit.Abstractions; + + /// + /// AuthTest + /// + [SuppressMessage("Analysis", "CA1506", Justification = "application composition root")] + public class AuthTest : TestBase + { + /// .ctor + /// ITestOutputHelper + /// TestFixture + public AuthTest(ITestOutputHelper output, TestFixture fixture) + : base(output, fixture) + { + } + + [Fact] + internal void Generate_jwt_auth_token() + { + var username = "qwerty"; + var permissions = new[] { "amazing_feature_42" }; + + var projectFileDirectory = SolutionExtensions.ProjectFile().Directory + ?? throw new InvalidOperationException("Project directory wasn't found"); + + var appSettings = projectFileDirectory + .StepInto("Settings") + .StepInto("JwtAuthToken") + .GetFile("appsettings", ".json"); + + var authEndpointConfiguration = new ConfigurationBuilder() + .AddJsonFile(appSettings.FullName) + .Build(); + + var tokenProvider = new JwtTokenProvider(new JwtSecurityTokenHandler(), authEndpointConfiguration.GetJwtAuthenticationConfiguration()); + + var token = tokenProvider.GenerateToken(username, permissions, TimeSpan.FromMinutes(5)); + Output.WriteLine(token); + + Assert.Equal(username, tokenProvider.GetUsername(token)); + Assert.True(permissions.SequenceEqual(tokenProvider.GetPermissions(token))); + } + + [Fact] + internal void Generate_basic_auth_token() + { + var username = "qwerty"; + var password = "12345678"; + + var token = (username, password).EncodeBasicAuth(); + var credentials = token.DecodeBasicAuth(); + + Output.WriteLine(token); + + Assert.Equal(username, credentials.username); + Assert.Equal(password, credentials.password); + } + } +} \ No newline at end of file diff --git a/tests/Tests/GenericHost.Test/DataAccessTest.cs b/tests/Tests/GenericHost.Test/DataAccessTest.cs new file mode 100644 index 00000000..f932b769 --- /dev/null +++ b/tests/Tests/GenericHost.Test/DataAccessTest.cs @@ -0,0 +1,2117 @@ +namespace SpaceEngineers.Core.GenericHost.Test +{ + using System; + using System.Collections.Generic; + using System.Data; + using System.Diagnostics.CodeAnalysis; + using System.IO; + using System.Linq; + using System.Reflection; + using System.Threading; + using System.Threading.Tasks; + using AuthEndpoint.Contract; + using AuthEndpoint.Host; + using Basics; + using Basics.Primitives; + using CompositionRoot; + using CrossCuttingConcerns.Logging; + using CrossCuttingConcerns.Settings; + using DataAccess.Orm.Sql.Connection; + using DataAccess.Orm.Sql.Exceptions; + using DataAccess.Orm.Sql.Linq; + using DataAccess.Orm.Sql.Migrations.Model; + using DataAccess.Orm.Sql.Model; + using DataAccess.Orm.Sql.Model.Attributes; + using DataAccess.Orm.Sql.Postgres.Connection; + using DataAccess.Orm.Sql.Settings; + using DataAccess.Orm.Sql.Transaction; + using DatabaseEntities; + using DatabaseEntities.Relations; + using Extensions; + using GenericDomain.EventSourcing.Sql; + using GenericEndpoint.Api.Abstractions; + using GenericEndpoint.Authorization; + using GenericEndpoint.Authorization.Host; + using GenericEndpoint.Authorization.Web.Host; + using GenericEndpoint.DataAccess.Sql.Deduplication; + using GenericEndpoint.DataAccess.Sql.Host; + using GenericEndpoint.DataAccess.Sql.Postgres.Host; + using GenericEndpoint.DataAccess.Sql.Settings; + using GenericEndpoint.EventSourcing.Host; + using GenericEndpoint.Host; + using GenericEndpoint.Host.Builder; + using GenericEndpoint.Messaging; + using GenericEndpoint.Messaging.MessageHeaders; + using IntegrationTransport.Api; + using IntegrationTransport.Api.Abstractions; + using IntegrationTransport.Host; + using IntegrationTransport.RabbitMQ; + using IntegrationTransport.RabbitMQ.Settings; + using JwtAuthentication; + using MessageHandlers; + using Messages; + using Microsoft.Extensions.Hosting; + using Microsoft.Extensions.Logging; + using Mocks; + using Registrations; + using SpaceEngineers.Core.Test.Api; + using SpaceEngineers.Core.Test.Api.ClassFixtures; + using StartupActions; + using Xunit; + using Xunit.Abstractions; + using IntegrationMessage = GenericEndpoint.DataAccess.Sql.Deduplication.IntegrationMessage; + using User = DatabaseEntities.Relations.User; + + /// + /// Endpoint data access tests + /// + [SuppressMessage("Analysis", "CA1506", Justification = "application composition root")] + public class EndpointDataAccessTests : TestBase + { + /// .cctor + /// ITestOutputHelper + /// TestFixture + public EndpointDataAccessTests(ITestOutputHelper output, TestFixture fixture) + : base(output, fixture) + { + } + + /// + /// Test cases for endpoint data access tests + /// + /// Test cases + public static IEnumerable BuildHostEndpointDataAccessTestData() + { + var timeout = TimeSpan.FromSeconds(60); + + var projectFileDirectory = SolutionExtensions.ProjectFile().Directory + ?? throw new InvalidOperationException("Project directory wasn't found"); + + var settingsDirectory = projectFileDirectory + .StepInto("Settings") + .StepInto(nameof(HostBuilderTests)); + + var inMemoryIntegrationTransportIdentity = IntegrationTransport.InMemory.Identity.TransportIdentity(); + + var useInMemoryIntegrationTransport = new Func( + static (hostBuilder, transportIdentity) => hostBuilder.UseInMemoryIntegrationTransport(transportIdentity)); + + var rabbitMqIntegrationTransportIdentity = IntegrationTransport.RabbitMQ.Identity.TransportIdentity(); + + var useRabbitMqIntegrationTransport = new Func( + static (hostBuilder, transportIdentity) => hostBuilder.UseRabbitMqIntegrationTransport(transportIdentity)); + + var integrationTransportProviders = new[] + { + (inMemoryIntegrationTransportIdentity, useInMemoryIntegrationTransport), + (rabbitMqIntegrationTransportIdentity, useRabbitMqIntegrationTransport) + }; + + var dataAccessProviders = new Func?, IEndpointBuilder>[] + { + (builder, dataAccessOptions) => builder.WithPostgreSqlDataAccess(dataAccessOptions) + }; + + var eventSourcingProviders = new Func[] + { + builder => builder.WithSqlEventSourcing() + }; + + return integrationTransportProviders + .SelectMany(transport => dataAccessProviders + .SelectMany(withDataAccess => eventSourcingProviders + .Select(withEventSourcing => + { + var (transportIdentity, useTransport) = transport; + + return new object[] + { + settingsDirectory, + transportIdentity, + useTransport, + withDataAccess, + withEventSourcing, + timeout + }; + }))); + } + + /// + /// Test cases for endpoint data access tests + /// + /// Test cases + public static IEnumerable EndpointDataAccessTestData() + { + Func settingsDirectoryProducer = + testDirectory => + { + var projectFileDirectory = SolutionExtensions.ProjectFile().Directory + ?? throw new InvalidOperationException("Project directory wasn't found"); + + return projectFileDirectory + .StepInto("Settings") + .StepInto(testDirectory); + }; + + var inMemoryIntegrationTransportIdentity = IntegrationTransport.InMemory.Identity.TransportIdentity(); + + var useInMemoryIntegrationTransport = new Func>( + static _ => (hostBuilder, transportIdentity, _) => hostBuilder.UseInMemoryIntegrationTransport( + transportIdentity, + options => options + .WithManualRegistrations(new MessagesCollectorManualRegistration()))); + + var rabbitMqIntegrationTransportIdentity = IntegrationTransport.RabbitMQ.Identity.TransportIdentity(); + + var useRabbitMqIntegrationTransport = new Func>( + static isolationLevel => (hostBuilder, transportIdentity, settingsDirectory) => hostBuilder.UseRabbitMqIntegrationTransport( + transportIdentity, + builder => builder + .WithManualRegistrations(new PurgeRabbitMqQueuesManualRegistration()) + .WithManualRegistrations(new MessagesCollectorManualRegistration()) + .WithManualRegistrations(new VirtualHostManualRegistration(settingsDirectory.Name + isolationLevel)))); + + var integrationTransportProviders = new[] + { + (inMemoryIntegrationTransportIdentity, useInMemoryIntegrationTransport), + (rabbitMqIntegrationTransportIdentity, useRabbitMqIntegrationTransport) + }; + + var dataAccessProviders = new Func?, IEndpointBuilder>[] + { + (builder, dataAccessOptions) => builder.WithPostgreSqlDataAccess(dataAccessOptions) + }; + + var eventSourcingProviders = new Func[] + { + builder => builder.WithSqlEventSourcing() + }; + + var isolationLevels = new[] + { + IsolationLevel.Snapshot, + IsolationLevel.ReadCommitted + }; + + return integrationTransportProviders + .SelectMany(transport => + { + var (transportIdentity, useTransport) = transport; + + return dataAccessProviders + .SelectMany(withDataAccess => eventSourcingProviders + .SelectMany(withEventSourcing => isolationLevels + .Select(isolationLevel => new object[] + { + settingsDirectoryProducer, + transportIdentity, + useTransport(isolationLevel), + withDataAccess, + withEventSourcing, + isolationLevel + }))); + }); + } + + [Theory(Timeout = 60_000)] + [MemberData(nameof(BuildHostEndpointDataAccessTestData))] + internal async Task Migration_generates_database_model_changes( + DirectoryInfo settingsDirectory, + TransportIdentity transportIdentity, + Func useTransport, + Func?, IEndpointBuilder> withDataAccess, + Func withEventSourcing, + TimeSpan timeout) + { + var databaseEntities = new[] + { + typeof(DatabaseEntity), + typeof(ComplexDatabaseEntity), + typeof(Community), + typeof(Participant), + typeof(Blog), + typeof(Post), + typeof(User) + }; + + var startupActions = new[] + { + typeof(RecreatePostgreSqlDatabaseHostedServiceStartupAction) + }; + + var additionalOurTypes = databaseEntities + .Concat(startupActions) + .ToArray(); + + var host = Fixture + .CreateHostBuilder() + .UseIntegrationTransport(transportIdentity, useTransport) + .UseEndpoint(TestIdentity.Endpoint10, + builder => withEventSourcing(withDataAccess(builder, options => options + .ExecuteMigrations())) + .ModifyContainerOptions(options => options + .WithAdditionalOurTypes(additionalOurTypes) + .WithAdditionalOurTypes(typeof(RecreatePostgreSqlDatabaseHostedServiceStartupAction))) + .BuildOptions()) + .BuildHost(settingsDirectory); + + using (host) + using (var cts = new CancellationTokenSource(timeout)) + { + var endpointContainer = host.GetEndpointDependencyContainer(TestIdentity.Endpoint10); + + await endpointContainer + .Resolve() + .Run(cts.Token) + .ConfigureAwait(false); + + var modelProvider = endpointContainer.Resolve(); + + var actualModel = await endpointContainer.InvokeWithinTransaction( + false, + endpointContainer.Resolve().BuildModel, + cts.Token) + .ConfigureAwait(false); + + databaseEntities = endpointContainer + .Resolve() + .DatabaseEntities() + .ToArray(); + + var expectedModel = await endpointContainer + .Resolve() + .BuildModel(databaseEntities, cts.Token) + .ConfigureAwait(false); + + var unorderedModelChanges = endpointContainer + .Resolve() + .ExtractDiff(actualModel, expectedModel); + + var modelChanges = endpointContainer + .Resolve() + .Sort(unorderedModelChanges) + .ToArray(); + + modelChanges.Each((change, i) => Output.WriteLine($"[{i}] {change}")); + + var databaseConnectionProvider = endpointContainer.Resolve(); + + if (databaseConnectionProvider.GetType() == typeof(DatabaseConnectionProvider)) + { + var assertions = new Action[] + { + index => AssertCreateSchema(modelChanges, index, nameof(GenericEndpoint.DataAccess.Sql.Deduplication)), + index => AssertCreateSchema(modelChanges, index, nameof(GenericEndpoint.EventSourcing)), + index => AssertCreateSchema(modelChanges, index, nameof(GenericHost) + nameof(Test)), + index => AssertCreateSchema(modelChanges, index, nameof(DataAccess.Orm.Sql.Migrations)), + index => AssertCreateEnumType(modelChanges, index, nameof(GenericHost) + nameof(Test), nameof(EnEnum), nameof(EnEnum.One), nameof(EnEnum.Two), nameof(EnEnum.Three)), + index => AssertCreateEnumType(modelChanges, index, nameof(GenericHost) + nameof(Test), nameof(EnEnumFlags), nameof(EnEnumFlags.A), nameof(EnEnumFlags.B), nameof(EnEnumFlags.C)), + index => AssertCreateEnumType(modelChanges, index, nameof(DataAccess.Orm.Sql.Migrations), nameof(EnColumnConstraintType), nameof(EnColumnConstraintType.PrimaryKey), nameof(EnColumnConstraintType.ForeignKey)), + index => AssertCreateEnumType(modelChanges, index, nameof(DataAccess.Orm.Sql.Migrations), nameof(EnTriggerEvent), nameof(EnTriggerEvent.Insert), nameof(EnTriggerEvent.Update), nameof(EnTriggerEvent.Delete)), + index => AssertCreateEnumType(modelChanges, index, nameof(DataAccess.Orm.Sql.Migrations), nameof(EnTriggerType), nameof(EnTriggerType.Before), nameof(EnTriggerType.After)), + index => + { + AssertCreateTable( + modelProvider, + modelChanges, + index, + nameof(GenericEndpoint.DataAccess.Sql.Deduplication), + typeof(GenericEndpoint.DataAccess.Sql.Deduplication.IntegrationMessage), + new[] + { + (nameof(GenericEndpoint.DataAccess.Sql.Deduplication.IntegrationMessage.PrimaryKey), "not null primary key"), + (nameof(GenericEndpoint.DataAccess.Sql.Deduplication.IntegrationMessage.Version), "not null"), + (nameof(GenericEndpoint.DataAccess.Sql.Deduplication.IntegrationMessage.Payload), "not null"), + (nameof(GenericEndpoint.DataAccess.Sql.Deduplication.IntegrationMessage.ReflectedType), "not null") + }, + new[] + { + nameof(GenericEndpoint.DataAccess.Sql.Deduplication.IntegrationMessage.Headers) + }); + }, + index => + { + AssertCreateTable( + modelProvider, + modelChanges, + index, + nameof(GenericEndpoint.DataAccess.Sql.Deduplication), + typeof(IntegrationMessageHeader), + new[] + { + (nameof(IntegrationMessageHeader.PrimaryKey), "not null primary key"), + (nameof(IntegrationMessageHeader.Version), "not null"), + (nameof(IntegrationMessageHeader.Message), $@"not null references ""{nameof(GenericEndpoint.DataAccess.Sql.Deduplication)}"".""{nameof(GenericEndpoint.DataAccess.Sql.Deduplication.IntegrationMessage)}"" (""{nameof(IUniqueIdentified.PrimaryKey)}"") on delete cascade"), + (nameof(IntegrationMessageHeader.Payload), "not null") + }, + Array.Empty()); + }, + index => + { + AssertCreateTable( + modelProvider, + modelChanges, + index, + nameof(GenericDomain.EventSourcing), + typeof(DatabaseDomainEvent), + new[] + { + (nameof(DatabaseDomainEvent.PrimaryKey), "not null primary key"), + (nameof(DatabaseDomainEvent.Version), "not null"), + (nameof(DatabaseDomainEvent.AggregateId), "not null"), + (nameof(DatabaseDomainEvent.Index), "not null"), + (nameof(DatabaseDomainEvent.Timestamp), "not null"), + (nameof(DatabaseDomainEvent.DomainEvent), "not null") + }, + Array.Empty()); + }, + index => + { + AssertCreateTable( + modelProvider, + modelChanges, + index, + nameof(GenericHost) + nameof(Test), + typeof(Blog), + new[] + { + (nameof(Blog.PrimaryKey), "not null primary key"), + (nameof(Blog.Version), "not null"), + (nameof(Blog.Theme), "not null") + }, + new[] + { + nameof(Blog.Posts) + }); + }, + index => + { + AssertCreateTable( + modelProvider, + modelChanges, + index, + nameof(GenericHost) + nameof(Test), + typeof(Community), + new[] + { + (nameof(Community.PrimaryKey), "not null primary key"), + (nameof(Community.Version), "not null"), + (nameof(Community.Name), "not null") + }, + new[] + { + nameof(Community.Participants) + }); + }, + index => + { + AssertCreateTable( + modelProvider, + modelChanges, + index, + nameof(GenericHost) + nameof(Test), + typeof(DatabaseEntity), + new[] + { + (nameof(DatabaseEntity.PrimaryKey), "not null primary key"), + (nameof(DatabaseEntity.Version), "not null"), + (nameof(DatabaseEntity.BooleanField), "not null"), + (nameof(DatabaseEntity.StringField), "not null"), + (nameof(DatabaseEntity.NullableStringField), string.Empty), + (nameof(DatabaseEntity.IntField), "not null"), + (nameof(DatabaseEntity.Enum), "not null") + }, + Array.Empty()); + }, + index => + { + AssertCreateTable( + modelProvider, + modelChanges, + index, + nameof(GenericHost) + nameof(Test), + typeof(Participant), + new[] + { + (nameof(Participant.PrimaryKey), "not null primary key"), + (nameof(Participant.Version), "not null"), + (nameof(Participant.Name), "not null") + }, + new[] + { + nameof(Participant.Communities) + }); + }, + index => + { + AssertCreateTable( + modelProvider, + modelChanges, + index, + nameof(GenericHost) + nameof(Test), + typeof(User), + new[] + { + (nameof(User.PrimaryKey), "not null primary key"), + (nameof(User.Version), "not null"), + (nameof(User.Nickname), "not null") + }, + Array.Empty()); + }, + index => + { + AssertCreateTable( + modelProvider, + modelChanges, + index, + nameof(DataAccess.Orm.Sql.Migrations), + typeof(AppliedMigration), + new[] + { + (nameof(AppliedMigration.PrimaryKey), "not null primary key"), + (nameof(AppliedMigration.Version), "not null"), + (nameof(AppliedMigration.DateTime), "not null"), + (nameof(AppliedMigration.CommandText), "not null"), + (nameof(AppliedMigration.Name), "not null") + }, + Array.Empty()); + }, + index => + { + AssertCreateTable( + modelProvider, + modelChanges, + index, + nameof(DataAccess.Orm.Sql.Migrations), + typeof(FunctionView), + new[] + { + (nameof(FunctionView.PrimaryKey), "not null primary key"), + (nameof(FunctionView.Version), "not null"), + (nameof(FunctionView.Schema), "not null"), + (nameof(FunctionView.Function), "not null"), + (nameof(FunctionView.Definition), "not null") + }, + Array.Empty()); + }, + index => + { + AssertCreateTable( + modelProvider, + modelChanges, + index, + nameof(DataAccess.Orm.Sql.Migrations), + typeof(SqlView), + new[] + { + (nameof(SqlView.PrimaryKey), "not null primary key"), + (nameof(SqlView.Version), "not null"), + (nameof(SqlView.Schema), "not null"), + (nameof(SqlView.View), "not null"), + (nameof(SqlView.Query), "not null") + }, + Array.Empty()); + }, + index => + { + AssertCreateTable( + modelProvider, + modelChanges, + index, + nameof(GenericEndpoint.DataAccess.Sql.Deduplication), + typeof(InboxMessage), + new[] + { + (nameof(InboxMessage.PrimaryKey), "not null primary key"), + (nameof(InboxMessage.Version), "not null"), + (nameof(InboxMessage.Message), $@"not null references ""{nameof(GenericEndpoint.DataAccess.Sql.Deduplication)}"".""{nameof(IntegrationMessage)}"" (""{nameof(IUniqueIdentified.PrimaryKey)}"") on delete cascade"), + (nameof(InboxMessage.EndpointLogicalName), "not null"), + (nameof(InboxMessage.EndpointInstanceName), "not null"), + (nameof(InboxMessage.IsError), "not null"), + (nameof(InboxMessage.Handled), "not null") + }, + Array.Empty()); + }, + index => + { + AssertCreateMtmTable( + modelProvider, + modelChanges, + index, + nameof(GenericEndpoint.DataAccess.Sql.Deduplication), + $"{nameof(IntegrationMessage)}_{nameof(IntegrationMessageHeader)}", + new[] + { + (nameof(BaseMtmDatabaseEntity.Left), $@"not null references ""{nameof(GenericEndpoint.DataAccess.Sql.Deduplication)}"".""{nameof(IntegrationMessage)}"" (""{nameof(IUniqueIdentified.PrimaryKey)}"") on delete cascade"), + (nameof(BaseMtmDatabaseEntity.Right), $@"not null references ""{nameof(GenericEndpoint.DataAccess.Sql.Deduplication)}"".""{nameof(IntegrationMessageHeader)}"" (""{nameof(IUniqueIdentified.PrimaryKey)}"") on delete cascade") + }); + }, + index => + { + AssertCreateTable( + modelProvider, + modelChanges, + index, + nameof(GenericEndpoint.DataAccess.Sql.Deduplication), + typeof(OutboxMessage), + new[] + { + (nameof(OutboxMessage.PrimaryKey), "not null primary key"), + (nameof(OutboxMessage.Version), "not null"), + (nameof(OutboxMessage.OutboxId), "not null"), + (nameof(OutboxMessage.Timestamp), "not null"), + (nameof(OutboxMessage.EndpointLogicalName), "not null"), + (nameof(OutboxMessage.EndpointInstanceName), "not null"), + (nameof(OutboxMessage.Message), $@"not null references ""{nameof(GenericEndpoint.DataAccess.Sql.Deduplication)}"".""{nameof(IntegrationMessage)}"" (""{nameof(IUniqueIdentified.PrimaryKey)}"") on delete cascade"), + (nameof(OutboxMessage.Sent), "not null") + }, + Array.Empty()); + }, + index => + { + AssertCreateMtmTable( + modelProvider, + modelChanges, + index, + nameof(GenericHost) + nameof(Test), + $"{nameof(Community)}_{nameof(Participant)}", + new[] + { + (nameof(BaseMtmDatabaseEntity.Left), $@"not null references ""{nameof(GenericHost) + nameof(Test)}"".""{nameof(Community)}"" (""{nameof(IUniqueIdentified.PrimaryKey)}"") on delete cascade"), + (nameof(BaseMtmDatabaseEntity.Right), $@"not null references ""{nameof(GenericHost) + nameof(Test)}"".""{nameof(Participant)}"" (""{nameof(IUniqueIdentified.PrimaryKey)}"") on delete cascade") + }); + }, + index => + { + AssertCreateTable( + modelProvider, + modelChanges, + index, + nameof(GenericHost) + nameof(Test), + typeof(ComplexDatabaseEntity), + new[] + { + (nameof(ComplexDatabaseEntity.PrimaryKey), "not null primary key"), + (nameof(ComplexDatabaseEntity.Version), "not null"), + (nameof(ComplexDatabaseEntity.Number), "not null"), + (nameof(ComplexDatabaseEntity.NullableNumber), string.Empty), + (nameof(ComplexDatabaseEntity.Identifier), "not null"), + (nameof(ComplexDatabaseEntity.NullableIdentifier), string.Empty), + (nameof(ComplexDatabaseEntity.Boolean), "not null"), + (nameof(ComplexDatabaseEntity.NullableBoolean), string.Empty), + (nameof(ComplexDatabaseEntity.DateTime), "not null"), + (nameof(ComplexDatabaseEntity.NullableDateTime), string.Empty), + (nameof(ComplexDatabaseEntity.TimeSpan), "not null"), + (nameof(ComplexDatabaseEntity.NullableTimeSpan), string.Empty), + (nameof(ComplexDatabaseEntity.DateOnly), "not null"), + (nameof(ComplexDatabaseEntity.NullableDateOnly), string.Empty), + (nameof(ComplexDatabaseEntity.TimeOnly), "not null"), + (nameof(ComplexDatabaseEntity.NullableTimeOnly), string.Empty), + (nameof(ComplexDatabaseEntity.ByteArray), "not null"), + (nameof(ComplexDatabaseEntity.String), "not null"), + (nameof(ComplexDatabaseEntity.NullableString), string.Empty), + (nameof(ComplexDatabaseEntity.Enum), "not null"), + (nameof(ComplexDatabaseEntity.NullableEnum), string.Empty), + (nameof(ComplexDatabaseEntity.EnumFlags), "not null"), + (nameof(ComplexDatabaseEntity.NullableEnumFlags), string.Empty), + (nameof(ComplexDatabaseEntity.EnumArray), "not null"), + (nameof(ComplexDatabaseEntity.NullableEnumArray), "not null"), + (nameof(ComplexDatabaseEntity.StringArray), "not null"), + (nameof(ComplexDatabaseEntity.NullableStringArray), "not null"), + (nameof(ComplexDatabaseEntity.DateTimeArray), "not null"), + (nameof(ComplexDatabaseEntity.NullableDateTimeArray), "not null"), + (nameof(ComplexDatabaseEntity.Json), "not null"), + (nameof(ComplexDatabaseEntity.NullableJson), string.Empty), + (nameof(ComplexDatabaseEntity.Relation), $@"not null references ""{nameof(GenericHost) + nameof(Test)}"".""{nameof(Blog)}"" (""{nameof(IUniqueIdentified.PrimaryKey)}"") on delete no action"), + (nameof(ComplexDatabaseEntity.NullableRelation), $@"references ""{nameof(GenericHost) + nameof(Test)}"".""{nameof(Blog)}"" (""{nameof(IUniqueIdentified.PrimaryKey)}"") on delete no action") + }, + Array.Empty()); + }, + index => + { + AssertCreateTable( + modelProvider, + modelChanges, + index, + nameof(GenericHost) + nameof(Test), + typeof(Post), + new[] + { + (nameof(Post.PrimaryKey), "not null primary key"), + (nameof(Post.Version), "not null"), + (nameof(Post.DateTime), "not null"), + (nameof(Post.Text), "not null"), + (nameof(Post.Blog), $@"not null references ""{nameof(GenericHost) + nameof(Test)}"".""{nameof(Blog)}"" (""{nameof(IUniqueIdentified.PrimaryKey)}"") on delete cascade"), + (nameof(Post.User), $@"not null references ""{nameof(GenericHost) + nameof(Test)}"".""{nameof(User)}"" (""{nameof(IUniqueIdentified.PrimaryKey)}"") on delete restrict") + }, + Array.Empty()); + }, + index => + { + AssertCreateMtmTable( + modelProvider, + modelChanges, + index, + nameof(GenericHost) + nameof(Test), + $"{nameof(Blog)}_{nameof(Post)}", + new[] + { + (nameof(BaseMtmDatabaseEntity.Left), $@"not null references ""{nameof(GenericHost) + nameof(Test)}"".""{nameof(Blog)}"" (""{nameof(IUniqueIdentified.PrimaryKey)}"") on delete cascade"), + (nameof(BaseMtmDatabaseEntity.Right), $@"not null references ""{nameof(GenericHost) + nameof(Test)}"".""{nameof(Post)}"" (""{nameof(IUniqueIdentified.PrimaryKey)}"") on delete cascade") + }); + }, + index => AssertCreateView(modelChanges, index, nameof(DatabaseColumn)), + index => AssertCreateView(modelChanges, index, nameof(DatabaseColumnConstraint)), + index => AssertCreateView(modelChanges, index, nameof(DatabaseEnumType)), + index => AssertCreateView(modelChanges, index, nameof(DatabaseFunction)), + index => AssertCreateView(modelChanges, index, nameof(DatabaseIndexColumn)), + index => AssertCreateView(modelChanges, index, nameof(DatabaseSchema)), + index => AssertCreateView(modelChanges, index, nameof(DatabaseTrigger)), + index => AssertCreateView(modelChanges, index, nameof(DatabaseView)), + index => AssertCreateIndex(modelProvider, modelChanges, index, true, null, nameof(GenericEndpoint.DataAccess.Sql.Deduplication), $"{nameof(IntegrationMessage)}_{nameof(IntegrationMessageHeader)}", new[] { nameof(BaseMtmDatabaseEntity.Left), nameof(BaseMtmDatabaseEntity.Right) }, Array.Empty()), + index => AssertCreateIndex(modelProvider, modelChanges, index, true, null, nameof(GenericEndpoint.EventSourcing), nameof(DatabaseDomainEvent), new[] { nameof(DatabaseDomainEvent.AggregateId), nameof(DatabaseDomainEvent.Index) }, Array.Empty()), + index => AssertCreateIndex(modelProvider, modelChanges, index, false, null, nameof(GenericEndpoint.EventSourcing), nameof(DatabaseDomainEvent), new[] { nameof(DatabaseDomainEvent.DomainEvent) }, Array.Empty()), + index => AssertCreateIndex(modelProvider, modelChanges, index, true, null, nameof(GenericHost) + nameof(Test), $"{nameof(Blog)}_{nameof(Post)}", new[] { nameof(BaseMtmDatabaseEntity.Left), nameof(BaseMtmDatabaseEntity.Right) }, Array.Empty()), + index => AssertCreateIndex(modelProvider, modelChanges, index, true, null, nameof(GenericHost) + nameof(Test), $"{nameof(Community)}_{nameof(Participant)}", new[] { nameof(BaseMtmDatabaseEntity.Left), nameof(BaseMtmDatabaseEntity.Right) }, Array.Empty()), + index => AssertCreateIndex(modelProvider, modelChanges, index, false, $@"""{nameof(DatabaseEntity.BooleanField)}""", nameof(GenericHost) + nameof(Test), nameof(DatabaseEntity), new[] { nameof(DatabaseEntity.StringField) }, new[] { nameof(DatabaseEntity.IntField) }), + index => AssertCreateIndex(modelProvider, modelChanges, index, true, null, nameof(DataAccess.Orm.Sql.Migrations), nameof(AppliedMigration), new[] { nameof(AppliedMigration.Name) }, Array.Empty()), + index => AssertCreateIndex(modelProvider, modelChanges, index, true, null, nameof(DataAccess.Orm.Sql.Migrations), nameof(DatabaseColumn), new[] { nameof(DatabaseColumn.Column), nameof(DatabaseColumn.Schema), nameof(DatabaseColumn.Table) }, Array.Empty()), + index => AssertCreateIndex(modelProvider, modelChanges, index, true, null, nameof(DataAccess.Orm.Sql.Migrations), nameof(DatabaseEnumType), new[] { nameof(DatabaseView.Schema), nameof(DatabaseEnumType.Type), nameof(DatabaseEnumType.Value) }, Array.Empty()), + index => AssertCreateIndex(modelProvider, modelChanges, index, true, null, nameof(DataAccess.Orm.Sql.Migrations), nameof(DatabaseFunction), new[] { nameof(DatabaseFunction.Function), nameof(DatabaseFunction.Schema) }, Array.Empty()), + index => AssertCreateIndex(modelProvider, modelChanges, index, true, null, nameof(DataAccess.Orm.Sql.Migrations), nameof(DatabaseIndexColumn), new[] { nameof(DatabaseIndexColumn.Column), nameof(DatabaseIndexColumn.Index), nameof(DatabaseIndexColumn.Schema), nameof(DatabaseIndexColumn.Table) }, Array.Empty()), + index => AssertCreateIndex(modelProvider, modelChanges, index, true, null, nameof(DataAccess.Orm.Sql.Migrations), nameof(DatabaseSchema), new[] { nameof(DatabaseSchema.Name) }, Array.Empty()), + index => AssertCreateIndex(modelProvider, modelChanges, index, true, null, nameof(DataAccess.Orm.Sql.Migrations), nameof(DatabaseTrigger), new[] { nameof(DatabaseTrigger.Schema), nameof(DatabaseTrigger.Trigger) }, Array.Empty()), + index => AssertCreateIndex(modelProvider, modelChanges, index, true, null, nameof(DataAccess.Orm.Sql.Migrations), nameof(DatabaseView), new[] { nameof(DatabaseView.Schema), nameof(DatabaseView.View) }, Array.Empty()), + index => AssertCreateIndex(modelProvider, modelChanges, index, true, null, nameof(DataAccess.Orm.Sql.Migrations), nameof(FunctionView), new[] { nameof(FunctionView.Function), nameof(FunctionView.Schema) }, Array.Empty()), + index => AssertCreateIndex(modelProvider, modelChanges, index, true, null, nameof(DataAccess.Orm.Sql.Migrations), nameof(SqlView), new[] { nameof(SqlView.Schema), nameof(SqlView.View) }, Array.Empty()), + index => AssertCreateFunction(modelProvider, modelChanges, index, nameof(GenericEndpoint.EventSourcing), nameof(AppendOnlyAttribute)), + index => AssertCreateTrigger(modelProvider, modelChanges, index, nameof(GenericEndpoint.EventSourcing), $"{nameof(DatabaseDomainEvent)}_aotrg", nameof(AppendOnlyAttribute)) + }; + + Assert.Equal(assertions.Length, modelChanges.Length); + + for (var i = 0; i < assertions.Length; i++) + { + assertions[i](i); + } + + static void AssertCreateTable( + IModelProvider modelProvider, + IModelChange[] modelChanges, + int index, + string schema, + Type table, + (string column, string constraints)[] columnsAssertions, + string[] mtmColumnsAssertions) + { + Assert.True(modelChanges[index] is CreateTable); + var createTable = (CreateTable)modelChanges[index]; + Assert.Equal($"{schema}.{table.Name}", $"{createTable.Schema}.{createTable.Table}"); + + AssertColumns(modelProvider, modelChanges, index, columnsAssertions); + AssertMtmColumns(modelProvider, modelChanges, index, mtmColumnsAssertions); + } + + static void AssertCreateMtmTable( + IModelProvider modelProvider, + IModelChange[] modelChanges, + int index, + string schema, + string table, + (string column, string constraints)[] columnsAssertions) + { + Assert.True(modelChanges[index] is CreateTable); + var createTable = (CreateTable)modelChanges[index]; + Assert.Equal($"{schema}.{table}", $"{createTable.Schema}.{createTable.Table}"); + + AssertColumns(modelProvider, modelChanges, index, columnsAssertions); + AssertMtmColumns(modelProvider, modelChanges, index, Array.Empty()); + } + + static void AssertColumns( + IModelProvider modelProvider, + IModelChange[] modelChanges, + int index, + (string column, string constraints)[] assertions) + { + Assert.True(modelChanges[index] is CreateTable); + var createTable = (CreateTable)modelChanges[index]; + Assert.True(modelProvider.TablesMap.ContainsKey(createTable.Schema)); + Assert.True(modelProvider.TablesMap[createTable.Schema].ContainsKey(createTable.Table)); + Assert.True(modelProvider.TablesMap[createTable.Schema][createTable.Table] is TableInfo); + var tableInfo = (TableInfo)modelProvider.TablesMap[createTable.Schema][createTable.Table]; + Assert.Equal(tableInfo.Columns.Values.Count(column => !column.IsMultipleRelation), assertions.Length); + + foreach (var (column, constraints) in assertions) + { + Assert.True(tableInfo.Columns.ContainsKey(column)); + var columnInfo = tableInfo.Columns[column]; + var actualConstraints = columnInfo.Constraints.ToString(" "); + Assert.Equal(actualConstraints, constraints, ignoreCase: true); + Assert.False(columnInfo.IsMultipleRelation); + + if (constraints.Contains("references", StringComparison.OrdinalIgnoreCase)) + { + Assert.NotNull(columnInfo.Relation); + } + else + { + Assert.Null(columnInfo.Relation); + } + } + } + + static void AssertMtmColumns( + IModelProvider modelProvider, + IModelChange[] modelChanges, + int index, + string[] columns) + { + Assert.True(modelChanges[index] is CreateTable); + var createTable = (CreateTable)modelChanges[index]; + Assert.True(modelProvider.TablesMap.ContainsKey(createTable.Schema)); + Assert.True(modelProvider.TablesMap[createTable.Schema].ContainsKey(createTable.Table)); + Assert.True(modelProvider.TablesMap[createTable.Schema][createTable.Table] is TableInfo); + var tableInfo = (TableInfo)modelProvider.TablesMap[createTable.Schema][createTable.Table]; + Assert.Equal(tableInfo.Columns.Values.Count(column => column.IsMultipleRelation), columns.Length); + + foreach (var column in columns) + { + Assert.True(tableInfo.Columns.ContainsKey(column)); + var columnInfo = tableInfo.Columns[column]; + Assert.True(columnInfo.IsMultipleRelation); + Assert.NotNull(columnInfo.Relation); + } + } + } + else + { + throw new NotSupportedException(databaseConnectionProvider.GetType().FullName); + } + + static void AssertCreateSchema( + IModelChange[] modelChanges, + int index, + string schema) + { + Assert.True(modelChanges[index] is CreateSchema); + var createSchema = (CreateSchema)modelChanges[index]; + Assert.Equal(createSchema.Schema, schema, ignoreCase: true); + } + + static void AssertCreateEnumType( + IModelChange[] modelChanges, + int index, + string schema, + string type, + params string[] values) + { + Assert.True(modelChanges[index] is CreateEnumType); + var createEnumType = (CreateEnumType)modelChanges[index]; + Assert.Equal(createEnumType.Schema, schema, ignoreCase: true); + Assert.Equal(createEnumType.Type, type, ignoreCase: true); + Assert.True(createEnumType.Values.SequenceEqual(values, StringComparer.Ordinal)); + } + + static void AssertCreateView( + IModelChange[] modelChanges, + int index, + string view) + { + Assert.True(modelChanges[index] is CreateView); + var createView = (CreateView)modelChanges[index]; + Assert.Equal(createView.View, view, ignoreCase: true); + } + + static void AssertCreateIndex( + IModelProvider modelProvider, + IModelChange[] modelChanges, + int index, + bool unique, + string? predicate, + string schema, + string table, + string[] columns, + string[] includedColumns) + { + Assert.True(modelChanges[index] is CreateIndex); + var createIndex = (CreateIndex)modelChanges[index]; + Assert.Equal(createIndex.Schema, schema, ignoreCase: true); + Assert.Equal(createIndex.Table, table, ignoreCase: true); + var indexName = (table, columns.ToString("_")).ToString("__"); + Assert.Equal(createIndex.Index, indexName, ignoreCase: true); + var indexInfo = modelProvider.TablesMap[schema][table].Indexes[createIndex.Index]; + Assert.True(includedColumns.OrderBy(column => column).SequenceEqual(indexInfo.IncludedColumns.Select(column => column.Name).OrderBy(column => column), StringComparer.OrdinalIgnoreCase)); + Assert.Equal(unique, indexInfo.Unique); + Assert.Equal(predicate, indexInfo.Predicate); + } + + static void AssertCreateFunction( + IModelProvider modelProvider, + IModelChange[] modelChanges, + int index, + string schema, + string function) + { + Assert.True(modelChanges[index] is CreateFunction); + var createFunction = (CreateFunction)modelChanges[index]; + Assert.Equal(createFunction.Schema, schema, ignoreCase: true); + Assert.Equal(createFunction.Function, function, ignoreCase: true); + } + + static void AssertCreateTrigger( + IModelProvider modelProvider, + IModelChange[] modelChanges, + int index, + string schema, + string trigger, + string function) + { + Assert.True(modelChanges[index] is CreateTrigger); + var createTrigger = (CreateTrigger)modelChanges[index]; + Assert.Equal(createTrigger.Schema, schema, ignoreCase: true); + Assert.Equal(createTrigger.Trigger, trigger, ignoreCase: true); + Assert.Equal(createTrigger.Function, function, ignoreCase: true); + } + } + } + + [Theory(Timeout = 60_000)] + [MemberData(nameof(BuildHostEndpointDataAccessTestData))] + internal async Task Equivalent_database_models_have_no_model_changes( + DirectoryInfo settingsDirectory, + TransportIdentity transportIdentity, + Func useTransport, + Func?, IEndpointBuilder> withDataAccess, + Func withEventSourcing, + TimeSpan timeout) + { + var startupActions = new[] + { + typeof(RecreatePostgreSqlDatabaseHostedServiceStartupAction) + }; + + var host = Fixture + .CreateHostBuilder() + .UseIntegrationTransport(transportIdentity, useTransport) + .UseEndpoint(TestIdentity.Endpoint10, + builder => withEventSourcing(withDataAccess(builder, options => options + .ExecuteMigrations())) + .ModifyContainerOptions(options => options + .WithAdditionalOurTypes(startupActions)) + .BuildOptions()) + .BuildHost(settingsDirectory); + + using (host) + using (var cts = new CancellationTokenSource(timeout)) + { + var endpointContainer = host.GetEndpointDependencyContainer(TestIdentity.Endpoint10); + + await endpointContainer + .Resolve() + .Run(cts.Token) + .ConfigureAwait(false); + + var actualModel = await endpointContainer.InvokeWithinTransaction( + false, + endpointContainer.Resolve().BuildModel, + cts.Token) + .ConfigureAwait(false); + + var databaseEntities = endpointContainer + .Resolve() + .DatabaseEntities() + .ToArray(); + + var expectedModel = await endpointContainer + .Resolve() + .BuildModel(databaseEntities, cts.Token) + .ConfigureAwait(false); + + var modelChanges = endpointContainer + .Resolve() + .ExtractDiff(actualModel, expectedModel); + + Assert.NotEmpty(modelChanges); + + modelChanges = endpointContainer + .Resolve() + .ExtractDiff(expectedModel, expectedModel); + + Assert.Empty(modelChanges); + } + } + + [Theory(Timeout = 60_000)] + [MemberData(nameof(EndpointDataAccessTestData))] + internal async Task Outbox_delivers_messages_in_background( + Func settingsDirectoryProducer, + TransportIdentity transportIdentity, + Func useTransport, + Func?, IEndpointBuilder> withDataAccess, + Func withEventSourcing, + IsolationLevel isolationLevel) + { + var settingsDirectory = settingsDirectoryProducer("OutboxDeliversMessagesInBackground"); + + var messageTypes = new[] + { + typeof(Request), + typeof(Reply) + }; + + var messageHandlerTypes = new[] + { + typeof(AlwaysReplyRequestHandler), + typeof(ReplyHandler) + }; + + var startupActions = new[] + { + typeof(RecreatePostgreSqlDatabaseHostedServiceStartupAction) + }; + + var additionalOurTypes = messageTypes + .Concat(messageHandlerTypes) + .Concat(startupActions) + .ToArray(); + + var host = Fixture + .CreateHostBuilder() + .UseIntegrationTransport(transportIdentity, useTransport, settingsDirectory) + .UseEndpoint(TestIdentity.Endpoint10, + builder => withEventSourcing(withDataAccess(builder, options => options.ExecuteMigrations())) + .ModifyContainerOptions(options => options + .WithAdditionalOurTypes(additionalOurTypes) + .WithManualRegistrations(new BackgroundOutboxDeliveryManualRegistration()) + .WithManualRegistrations(new IsolationLevelManualRegistration(isolationLevel))) + .BuildOptions()) + .BuildHost(settingsDirectory); + + await host + .RunTestHost(Output, TestCase, BackgroundOutboxDeliveryTestInternal(settingsDirectory, transportIdentity, isolationLevel)) + .ConfigureAwait(false); + + static Func BackgroundOutboxDeliveryTestInternal( + DirectoryInfo settingsDirectory, + TransportIdentity transportIdentity, + IsolationLevel isolationLevel) + { + return async (_, host, token) => + { + var transportDependencyContainer = host.GetIntegrationTransportDependencyContainer(transportIdentity); + var endpointDependencyContainer = host.GetEndpointDependencyContainer(TestIdentity.Endpoint10); + + var sqlDatabaseSettings = endpointDependencyContainer + .Resolve>() + .Get(); + + Assert.Equal(settingsDirectory.Name, sqlDatabaseSettings.Database); + Assert.Equal(isolationLevel, sqlDatabaseSettings.IsolationLevel); + Assert.Equal(1u, sqlDatabaseSettings.ConnectionPoolSize); + + if (transportDependencyContainer.Resolve() is RabbitMqIntegrationTransport) + { + var rabbitMqSettings = transportDependencyContainer + .Resolve>() + .Get(); + + Assert.Equal(settingsDirectory.Name + isolationLevel, rabbitMqSettings.VirtualHost); + } + + var outboxSettings = endpointDependencyContainer + .Resolve>() + .Get(); + + Assert.Equal(TimeSpan.FromSeconds(1), outboxSettings.OutboxDeliveryInterval); + + await using (transportDependencyContainer.OpenScopeAsync().ConfigureAwait(false)) + { + var collector = transportDependencyContainer.Resolve(); + + var awaiter = Task.WhenAll( + collector.WaitUntilMessageIsNotReceived(), + collector.WaitUntilMessageIsNotReceived(), + collector.WaitUntilErrorMessageIsNotReceived(message => message.HandlerType == typeof(ReplyHandler) && message.EndpointIdentity == TestIdentity.Endpoint10)); + + var integrationMessage = endpointDependencyContainer + .Resolve() + .CreateGeneralMessage( + new Request(42), + typeof(Request), + Array.Empty(), + null); + + await transportDependencyContainer + .Resolve() + .Enqueue(integrationMessage, token) + .ConfigureAwait(false); + + await awaiter.ConfigureAwait(false); + } + }; + } + } + + [Theory(Timeout = 60_000)] + [MemberData(nameof(EndpointDataAccessTestData))] + internal async Task Endpoint_applies_optimistic_concurrency_control( + Func settingsDirectoryProducer, + TransportIdentity transportIdentity, + Func useTransport, + Func?, IEndpointBuilder> withDataAccess, + Func withEventSourcing, + IsolationLevel isolationLevel) + { + var settingsDirectory = settingsDirectoryProducer("EndpointAppliesOptimisticConcurrencyControl"); + + var databaseEntities = new[] + { + typeof(DatabaseEntity) + }; + + var startupActions = new[] + { + typeof(RecreatePostgreSqlDatabaseHostedServiceStartupAction) + }; + + var additionalOurTypes = databaseEntities + .Concat(startupActions) + .ToArray(); + + var host = Fixture + .CreateHostBuilder() + .UseIntegrationTransport(transportIdentity, useTransport, settingsDirectory) + .UseEndpoint(TestIdentity.Endpoint10, + builder => withEventSourcing(withDataAccess(builder, options => options.ExecuteMigrations())) + .ModifyContainerOptions(options => options + .WithAdditionalOurTypes(additionalOurTypes) + .WithManualRegistrations(new IsolationLevelManualRegistration(isolationLevel))) + .BuildOptions()) + .BuildHost(settingsDirectory); + + await host + .RunTestHost(Output, TestCase, OptimisticConcurrencyTestInternal(settingsDirectory, transportIdentity, isolationLevel)) + .ConfigureAwait(false); + + static Func OptimisticConcurrencyTestInternal( + DirectoryInfo settingsDirectory, + TransportIdentity transportIdentity, + IsolationLevel isolationLevel) + { + return async (_, host, token) => + { + var transportDependencyContainer = host.GetIntegrationTransportDependencyContainer(transportIdentity); + var endpointDependencyContainer = host.GetEndpointDependencyContainer(TestIdentity.Endpoint10); + + var sqlDatabaseSettings = endpointDependencyContainer + .Resolve>() + .Get(); + + Assert.Equal(settingsDirectory.Name, sqlDatabaseSettings.Database); + Assert.Equal(isolationLevel, sqlDatabaseSettings.IsolationLevel); + Assert.Equal(3u, sqlDatabaseSettings.ConnectionPoolSize); + + if (transportDependencyContainer.Resolve() is RabbitMqIntegrationTransport) + { + var rabbitMqSettings = transportDependencyContainer + .Resolve>() + .Get(); + + Assert.Equal(settingsDirectory.Name + isolationLevel, rabbitMqSettings.VirtualHost); + } + + var primaryKey = Guid.NewGuid(); + + // #1 - create/create + { + Exception? exception = null; + + try + { + await Task.WhenAll( + CreateEntity(endpointDependencyContainer, primaryKey, token), + CreateEntity(endpointDependencyContainer, primaryKey, token)) + .ConfigureAwait(false); + } + catch (DatabaseException databaseException) when (databaseException is not DatabaseConcurrentUpdateException) + { + exception = databaseException; + } + + Assert.NotNull(exception); + Assert.True(exception is DatabaseException); + Assert.NotNull(exception.InnerException); + Assert.Contains(exception.Flatten(), ex => ex.IsUniqueViolation()); + + var entity = await ReadEntity(endpointDependencyContainer, primaryKey, token).ConfigureAwait(false); + + Assert.NotNull(entity); + Assert.NotEqual(default, entity.Version); + Assert.Equal(42, entity.IntField); + } + + // #2 - update/update + { + Exception? exception = null; + + try + { + var sync = new AsyncManualResetEvent(false); + + var updateTask1 = UpdateEntity(endpointDependencyContainer, primaryKey, sync, token); + var updateTask2 = UpdateEntity(endpointDependencyContainer, primaryKey, sync, token); + var syncTask = Task.Delay(TimeSpan.FromMilliseconds(100), token).ContinueWith(_ => sync.Set(), token, TaskContinuationOptions.ExecuteSynchronously, TaskScheduler.Default); + + await Task.WhenAll(updateTask1, updateTask2, syncTask).ConfigureAwait(false); + } + catch (DatabaseConcurrentUpdateException concurrentUpdateException) + { + exception = concurrentUpdateException; + } + + Assert.NotNull(exception); + + var entity = await ReadEntity(endpointDependencyContainer, primaryKey, token).ConfigureAwait(false); + + Assert.NotNull(entity); + Assert.NotEqual(default, entity.Version); + Assert.Equal(43, entity.IntField); + } + + // #3 - update/delete + { + var sync = new AsyncManualResetEvent(false); + + var updateTask = UpdateEntity(endpointDependencyContainer, primaryKey, sync, token); + var deleteTask = DeleteEntity(endpointDependencyContainer, primaryKey, sync, token); + var syncTask = Task.Delay(TimeSpan.FromMilliseconds(100), token).ContinueWith(_ => sync.Set(), token, TaskContinuationOptions.ExecuteSynchronously, TaskScheduler.Default); + + Exception? exception = null; + + try + { + await Task.WhenAll(updateTask, deleteTask, syncTask).ConfigureAwait(false); + } + catch (DatabaseConcurrentUpdateException concurrentUpdateException) + { + exception = concurrentUpdateException; + } + + Assert.NotNull(exception); + + var entity = await ReadEntity(endpointDependencyContainer, primaryKey, token).ConfigureAwait(false); + + if (updateTask.IsFaulted || deleteTask.IsCompletedSuccessfully) + { + Assert.Null(entity); + } + else + { + Assert.NotNull(entity); + Assert.NotEqual(default, entity.Version); + Assert.Equal(44, entity.IntField); + } + } + }; + } + + static async Task CreateEntity( + IDependencyContainer dependencyContainer, + Guid primaryKey, + CancellationToken token) + { + await using (dependencyContainer.OpenScopeAsync().ConfigureAwait(false)) + { + var transaction = dependencyContainer.Resolve(); + + long version; + + await using (await transaction.OpenScope(true, token).ConfigureAwait(false)) + { + var entity = DatabaseEntity.Generate(primaryKey); + + _ = await transaction + .Insert(new[] { entity }, EnInsertBehavior.Default) + .CachedExpression("4FC0EDC1-C658-4CA9-88A0-96DFA933AD7F") + .Invoke(token) + .ConfigureAwait(false); + + version = entity.Version; + } + + dependencyContainer + .Resolve() + .Debug($"{nameof(CreateEntity)}: {primaryKey} {version}"); + } + } + + static async Task ReadEntity( + IDependencyContainer dependencyContainer, + Guid primaryKey, + CancellationToken token) + { + await using (dependencyContainer.OpenScopeAsync().ConfigureAwait(false)) + { + var transaction = dependencyContainer.Resolve(); + + await using (await transaction.OpenScope(true, token).ConfigureAwait(false)) + { + return await transaction + .SingleOrDefault(primaryKey, token) + .ConfigureAwait(false); + } + } + } + + static async Task UpdateEntity( + IDependencyContainer dependencyContainer, + Guid primaryKey, + AsyncManualResetEvent sync, + CancellationToken token) + { + await using (dependencyContainer.OpenScopeAsync().ConfigureAwait(false)) + { + var transaction = dependencyContainer.Resolve(); + + await using (await transaction.OpenScope(true, token).ConfigureAwait(false)) + { + _ = await transaction + .Update() + .Set(entity => entity.IntField.Assign(entity.IntField + 1)) + .Where(entity => entity.PrimaryKey == primaryKey) + .CachedExpression("2CD18D09-05F8-4D80-AC19-96F7F524D28B") + .Invoke(token) + .ConfigureAwait(false); + + await sync + .WaitAsync(token) + .ConfigureAwait(false); + } + + dependencyContainer + .Resolve() + .Debug($"{nameof(UpdateEntity)}: {primaryKey}"); + } + } + + static async Task DeleteEntity( + IDependencyContainer dependencyContainer, + Guid primaryKey, + AsyncManualResetEvent sync, + CancellationToken token) + { + await using (dependencyContainer.OpenScopeAsync().ConfigureAwait(false)) + { + var transaction = dependencyContainer.Resolve(); + + await using (await transaction.OpenScope(true, token).ConfigureAwait(false)) + { + _ = await transaction + .Delete() + .Where(entity => entity.PrimaryKey == primaryKey) + .CachedExpression("2ADC594A-13EF-4F52-BD50-0F7FF62E5462") + .Invoke(token) + .ConfigureAwait(false); + + await sync + .WaitAsync(token) + .ConfigureAwait(false); + } + + dependencyContainer + .Resolve() + .Debug($"{nameof(DeleteEntity)}: {primaryKey}"); + } + } + } + + [Theory(Timeout = 60_000)] + [MemberData(nameof(EndpointDataAccessTestData))] + internal async Task Orm_tracks_entity_changes( + Func settingsDirectoryProducer, + TransportIdentity transportIdentity, + Func useTransport, + Func?, IEndpointBuilder> withDataAccess, + Func withEventSourcing, + IsolationLevel isolationLevel) + { + var settingsDirectory = settingsDirectoryProducer("OrmTracksEntityChanges"); + + var databaseEntities = new[] + { + typeof(DatabaseEntity) + }; + + var startupActions = new[] + { + typeof(RecreatePostgreSqlDatabaseHostedServiceStartupAction) + }; + + var additionalOurTypes = databaseEntities + .Concat(startupActions) + .ToArray(); + + var host = Fixture + .CreateHostBuilder() + .UseIntegrationTransport(transportIdentity, useTransport, settingsDirectory) + .UseEndpoint(TestIdentity.Endpoint10, + builder => withEventSourcing(withDataAccess(builder, options => options.ExecuteMigrations())) + .ModifyContainerOptions(options => options + .WithAdditionalOurTypes(additionalOurTypes) + .WithManualRegistrations(new IsolationLevelManualRegistration(isolationLevel))) + .BuildOptions()) + .BuildHost(settingsDirectory); + + await host + .RunTestHost(Output, TestCase, ReactiveTransactionalStoreTestInternal(settingsDirectory, transportIdentity, isolationLevel)) + .ConfigureAwait(false); + + static Func ReactiveTransactionalStoreTestInternal( + DirectoryInfo settingsDirectory, + TransportIdentity transportIdentity, + IsolationLevel isolationLevel) + { + return async (_, host, token) => + { + var transportDependencyContainer = host.GetIntegrationTransportDependencyContainer(transportIdentity); + var endpointDependencyContainer = host.GetEndpointDependencyContainer(TestIdentity.Endpoint10); + + var sqlDatabaseSettings = endpointDependencyContainer + .Resolve>() + .Get(); + + Assert.Equal(settingsDirectory.Name, sqlDatabaseSettings.Database); + Assert.Equal(isolationLevel, sqlDatabaseSettings.IsolationLevel); + Assert.Equal(1u, sqlDatabaseSettings.ConnectionPoolSize); + + if (transportDependencyContainer.Resolve() is RabbitMqIntegrationTransport) + { + var rabbitMqSettings = transportDependencyContainer + .Resolve>() + .Get(); + + Assert.Equal(settingsDirectory.Name + isolationLevel, rabbitMqSettings.VirtualHost); + } + + // [I] - update transactional store without explicit reads + await using (endpointDependencyContainer.OpenScopeAsync().ConfigureAwait(false)) + { + var transaction = endpointDependencyContainer.Resolve(); + var transactionalStore = endpointDependencyContainer.Resolve(); + + await using (await transaction.OpenScope(true, token).ConfigureAwait(false)) + { + var primaryKey = Guid.NewGuid(); + + // #0 - zero checks + Assert.False(transactionalStore.TryGetValue(primaryKey, out DatabaseEntity? storedEntry)); + Assert.Null(storedEntry); + + Assert.Null(await ReadEntity(transaction, primaryKey, token).ConfigureAwait(false)); + + // #1 - create + await CreateEntity(transaction, primaryKey, token).ConfigureAwait(false); + + Assert.True(transactionalStore.TryGetValue(primaryKey, out storedEntry)); + + Assert.NotNull(storedEntry); + Assert.NotEqual(default, storedEntry.Version); + Assert.Equal(primaryKey, storedEntry.PrimaryKey); + Assert.Equal(42, storedEntry.IntField); + + // #2 - update + await UpdateEntity(transaction, primaryKey, token).ConfigureAwait(false); + + Assert.True(transactionalStore.TryGetValue(primaryKey, out storedEntry)); + + Assert.NotNull(storedEntry); + Assert.NotEqual(default, storedEntry.Version); + Assert.Equal(primaryKey, storedEntry.PrimaryKey); + Assert.Equal(43, storedEntry.IntField); + + // #3 - delete + await DeleteEntity(transaction, primaryKey, token).ConfigureAwait(false); + + Assert.False(transactionalStore.TryGetValue(primaryKey, out storedEntry)); + Assert.Null(storedEntry); + + Assert.Null(await ReadEntity(transaction, primaryKey, token).ConfigureAwait(false)); + } + } + + // [II] - update transactional store through explicit reads + await using (endpointDependencyContainer.OpenScopeAsync().ConfigureAwait(false)) + { + var transaction = endpointDependencyContainer.Resolve(); + var transactionalStore = endpointDependencyContainer.Resolve(); + + await using (await transaction.OpenScope(true, token).ConfigureAwait(false)) + { + var primaryKey = Guid.NewGuid(); + + // #0 - zero checks + Assert.Null(await ReadEntity(transaction, primaryKey, token).ConfigureAwait(false)); + + Assert.False(transactionalStore.TryGetValue(primaryKey, out DatabaseEntity? storedEntry)); + Assert.Null(storedEntry); + + // #1 - create + await CreateEntity(transaction, primaryKey, token).ConfigureAwait(false); + + var entity = await ReadEntity(transaction, primaryKey, token).ConfigureAwait(false); + + Assert.NotNull(entity); + Assert.NotEqual(default, entity.Version); + Assert.Equal(primaryKey, entity.PrimaryKey); + Assert.Equal(42, entity.IntField); + + Assert.True(transactionalStore.TryGetValue(primaryKey, out storedEntry)); + + Assert.NotNull(storedEntry); + Assert.NotEqual(default, storedEntry.Version); + Assert.Equal(primaryKey, storedEntry.PrimaryKey); + Assert.Equal(42, storedEntry.IntField); + + Assert.Same(entity, storedEntry); + + // #2 - update + await UpdateEntity(transaction, primaryKey, token).ConfigureAwait(false); + + entity = await ReadEntity(transaction, primaryKey, token).ConfigureAwait(false); + + Assert.NotNull(entity); + Assert.NotEqual(default, entity.Version); + Assert.Equal(primaryKey, entity.PrimaryKey); + Assert.Equal(43, entity.IntField); + + Assert.True(transactionalStore.TryGetValue(primaryKey, out storedEntry)); + + Assert.NotNull(storedEntry); + Assert.NotEqual(default, storedEntry.Version); + Assert.Equal(primaryKey, storedEntry.PrimaryKey); + Assert.Equal(43, storedEntry.IntField); + + Assert.Same(entity, storedEntry); + + // #3 - delete + await DeleteEntity(transaction, primaryKey, token).ConfigureAwait(false); + + Assert.Null(await ReadEntity(transaction, primaryKey, token).ConfigureAwait(false)); + + Assert.False(transactionalStore.TryGetValue(primaryKey, out storedEntry)); + Assert.Null(storedEntry); + } + } + + // [III] - keep reactive reference + await using (endpointDependencyContainer.OpenScopeAsync().ConfigureAwait(false)) + { + var transaction = endpointDependencyContainer.Resolve(); + var transactionalStore = endpointDependencyContainer.Resolve(); + + await using (await transaction.OpenScope(true, token).ConfigureAwait(false)) + { + var primaryKey = Guid.NewGuid(); + + // #0 - zero checks + Assert.Null(await ReadEntity(transaction, primaryKey, token).ConfigureAwait(false)); + + Assert.False(transactionalStore.TryGetValue(primaryKey, out DatabaseEntity? storedEntry)); + Assert.Null(storedEntry); + + // #1 - create + await CreateEntity(transaction, primaryKey, token).ConfigureAwait(false); + + var entity = await ReadEntity(transaction, primaryKey, token).ConfigureAwait(false); + + Assert.NotNull(entity); + Assert.NotEqual(default, entity.Version); + Assert.Equal(primaryKey, entity.PrimaryKey); + Assert.Equal(42, entity.IntField); + + // #2 - update + await UpdateEntity(transaction, primaryKey, token).ConfigureAwait(false); + + Assert.NotNull(entity); + Assert.NotEqual(default, entity.Version); + Assert.Equal(primaryKey, entity.PrimaryKey); + Assert.Equal(43, entity.IntField); + + // #3 - delete + await DeleteEntity(transaction, primaryKey, token).ConfigureAwait(false); + + Assert.Null(await ReadEntity(transaction, primaryKey, token).ConfigureAwait(false)); + + Assert.False(transactionalStore.TryGetValue(primaryKey, out storedEntry)); + Assert.Null(storedEntry); + } + } + }; + } + + static async Task CreateEntity( + IDatabaseTransaction transaction, + Guid primaryKey, + CancellationToken token) + { + var entity = DatabaseEntity.Generate(primaryKey); + + _ = await transaction + .Insert(new[] { entity }, EnInsertBehavior.Default) + .CachedExpression("19ECB7D0-20CC-4AB1-819E-B40E6AC56E98") + .Invoke(token) + .ConfigureAwait(false); + } + + static async Task ReadEntity( + IDatabaseTransaction transaction, + Guid primaryKey, + CancellationToken token) + { + return await transaction + .SingleOrDefault(primaryKey, token) + .ConfigureAwait(false); + } + + static async Task UpdateEntity( + IDatabaseTransaction transaction, + Guid primaryKey, + CancellationToken token) + { + _ = await transaction + .Update() + .Set(entity => entity.IntField.Assign(entity.IntField + 1)) + .Where(entity => entity.PrimaryKey == primaryKey) + .CachedExpression("8391A774-81D3-40C7-980D-C5F9A874C4B1") + .Invoke(token) + .ConfigureAwait(false); + } + + static async Task DeleteEntity( + IDatabaseTransaction transaction, + Guid primaryKey, + CancellationToken token) + { + _ = await transaction + .Delete() + .Where(entity => entity.PrimaryKey == primaryKey) + .CachedExpression("32A01EBD-B109-434F-BC0D-5EDEB2341289") + .Invoke(token) + .ConfigureAwait(false); + } + } + + [Theory(Timeout = 60_000)] + [MemberData(nameof(EndpointDataAccessTestData))] + internal async Task Only_commands_can_introduce_changes( + Func settingsDirectoryProducer, + TransportIdentity transportIdentity, + Func useTransport, + Func?, IEndpointBuilder> withDataAccess, + Func withEventSourcing, + IsolationLevel isolationLevel) + { + var settingsDirectory = settingsDirectoryProducer("OnlyCommandsCanIntroduceChanges"); + + var messageTypes = new[] + { + typeof(Command), + typeof(Request), + typeof(Reply), + typeof(Event) + }; + + var messageHandlerTypes = new[] + { + typeof(IntroduceDatabaseChangesCommandHandler), + typeof(IntroduceDatabaseChangesRequestHandler), + typeof(IntroduceDatabaseChangesReplyHandler), + typeof(IntroduceDatabaseChangesEventHandler) + }; + + var databaseEntities = new[] + { + typeof(DatabaseEntity) + }; + + var startupActions = new[] + { + typeof(RecreatePostgreSqlDatabaseHostedServiceStartupAction) + }; + + var additionalOurTypes = messageTypes + .Concat(messageHandlerTypes) + .Concat(databaseEntities) + .Concat(startupActions) + .ToArray(); + + var host = Fixture + .CreateHostBuilder() + .UseIntegrationTransport(transportIdentity, useTransport, settingsDirectory) + .UseEndpoint(TestIdentity.Endpoint10, + builder => withEventSourcing(withDataAccess(builder, options => options.ExecuteMigrations())) + .ModifyContainerOptions(options => options + .WithAdditionalOurTypes(additionalOurTypes) + .WithManualRegistrations(new IsolationLevelManualRegistration(isolationLevel))) + .BuildOptions()) + .BuildHost(settingsDirectory); + + await host + .RunTestHost(Output, TestCase, OnlyCommandsCanIntroduceChangesInternal(settingsDirectory, transportIdentity, isolationLevel)) + .ConfigureAwait(false); + + static Func OnlyCommandsCanIntroduceChangesInternal( + DirectoryInfo settingsDirectory, + TransportIdentity transportIdentity, + IsolationLevel isolationLevel) + { + return async (output, host, token) => + { + var transportDependencyContainer = host.GetIntegrationTransportDependencyContainer(transportIdentity); + var endpointDependencyContainer = host.GetEndpointDependencyContainer(TestIdentity.Endpoint10); + + var sqlDatabaseSettings = endpointDependencyContainer + .Resolve>() + .Get(); + + Assert.Equal(settingsDirectory.Name, sqlDatabaseSettings.Database); + Assert.Equal(isolationLevel, sqlDatabaseSettings.IsolationLevel); + Assert.Equal(1u, sqlDatabaseSettings.ConnectionPoolSize); + + if (transportDependencyContainer.Resolve() is RabbitMqIntegrationTransport) + { + var rabbitMqSettings = transportDependencyContainer + .Resolve>() + .Get(); + + Assert.Equal(settingsDirectory.Name + isolationLevel, rabbitMqSettings.VirtualHost); + } + + await using (transportDependencyContainer.OpenScopeAsync().ConfigureAwait(false)) + { + var collector = transportDependencyContainer.Resolve(); + + var awaiter = collector.WaitUntilMessageIsNotReceived(); + + var integrationMessage = endpointDependencyContainer + .Resolve() + .CreateGeneralMessage( + new Command(42), + typeof(Command), + Array.Empty(), + null); + + await transportDependencyContainer + .Resolve() + .Enqueue(integrationMessage, token) + .ConfigureAwait(false); + + await awaiter.ConfigureAwait(false); + } + + await using (transportDependencyContainer.OpenScopeAsync().ConfigureAwait(false)) + { + var collector = transportDependencyContainer.Resolve(); + + var awaiter = collector.WaitUntilErrorMessageIsNotReceived( + exceptionPredicate: exception => + exception is InvalidOperationException && + exception.Message.Contains("only commands can introduce changes", StringComparison.OrdinalIgnoreCase)); + + var integrationMessage = endpointDependencyContainer + .Resolve() + .CreateGeneralMessage( + new Request(42), + typeof(Request), + Array.Empty(), + null); + + await transportDependencyContainer + .Resolve() + .Enqueue(integrationMessage, token) + .ConfigureAwait(false); + + await awaiter.ConfigureAwait(false); + } + + await using (transportDependencyContainer.OpenScopeAsync().ConfigureAwait(false)) + { + var collector = transportDependencyContainer.Resolve(); + + var awaiter = collector.WaitUntilErrorMessageIsNotReceived( + exceptionPredicate: exception => + exception is InvalidOperationException + && exception.Message.Contains("only commands can introduce changes", StringComparison.OrdinalIgnoreCase)); + + var request = new Request(42); + + var initiatorMessage = endpointDependencyContainer + .Resolve() + .CreateGeneralMessage( + request, + typeof(Request), + Array.Empty(), + null); + + var integrationMessage = endpointDependencyContainer + .Resolve() + .CreateGeneralMessage( + new Reply(request.Id), + typeof(Reply), + Array.Empty(), + initiatorMessage); + + await transportDependencyContainer + .Resolve() + .Enqueue(integrationMessage, token) + .ConfigureAwait(false); + + await awaiter.ConfigureAwait(false); + } + + await using (transportDependencyContainer.OpenScopeAsync().ConfigureAwait(false)) + { + var collector = transportDependencyContainer.Resolve(); + + var awaiter = collector.WaitUntilErrorMessageIsNotReceived( + exceptionPredicate: exception => + exception is InvalidOperationException + && exception.Message.Contains("only commands can introduce changes", StringComparison.OrdinalIgnoreCase)); + + var integrationMessage = endpointDependencyContainer + .Resolve() + .CreateGeneralMessage( + new Event(42), + typeof(Event), + Array.Empty(), + null); + + await transportDependencyContainer + .Resolve() + .Enqueue(integrationMessage, token) + .ConfigureAwait(false); + + await awaiter.ConfigureAwait(false); + } + }; + } + } + + // TODO: #205 - recode after rpc-transport implementation + [SuppressMessage("Analysis", "xUnit1004", Justification = "#205")] + [Theory(Skip = "#205", Timeout = 60_000)] + [MemberData(nameof(EndpointDataAccessTestData))] + internal async Task Messaging_requires_authorization( + Func settingsDirectoryProducer, + TransportIdentity transportIdentity, + Func useTransport, + Func?, IEndpointBuilder> withDataAccess, + Func withEventSourcing, + IsolationLevel isolationLevel) + { + var settingsDirectory = settingsDirectoryProducer("MessagingRequiresAuthorization"); + + var startupActions = new[] + { + typeof(RecreatePostgreSqlDatabaseHostedServiceStartupAction) + }; + + var host = Fixture + .CreateHostBuilder() + .UseIntegrationTransport(transportIdentity, useTransport, settingsDirectory) + .UseAuthEndpoint(builder => + withEventSourcing(withDataAccess(builder, options => options.ExecuteMigrations())) + .WithJwtAuthentication(builder.Context.Configuration) + .WithAuthorization() + .WithWebAuthorization() + .ModifyContainerOptions(options => options + .WithAdditionalOurTypes(startupActions) + .WithManualRegistrations(new IsolationLevelManualRegistration(isolationLevel))) + .BuildOptions()) + .BuildHost(settingsDirectory); + + await host + .RunTestHost(Output, TestCase, AuthorizeUserTestInternal(settingsDirectory, transportIdentity, isolationLevel)) + .ConfigureAwait(false); + + static Func AuthorizeUserTestInternal( + DirectoryInfo settingsDirectory, + TransportIdentity transportIdentity, + IsolationLevel isolationLevel) + { + return async (output, host, token) => + { + var transportDependencyContainer = host.GetIntegrationTransportDependencyContainer(transportIdentity); + var endpointDependencyContainer = host.GetEndpointDependencyContainer(AuthEndpoint.Contract.Identity.EndpointIdentity); + + var sqlDatabaseSettings = endpointDependencyContainer + .Resolve>() + .Get(); + + Assert.Equal(settingsDirectory.Name, sqlDatabaseSettings.Database); + Assert.Equal(isolationLevel, sqlDatabaseSettings.IsolationLevel); + Assert.Equal(1u, sqlDatabaseSettings.ConnectionPoolSize); + + if (transportDependencyContainer.Resolve() is RabbitMqIntegrationTransport) + { + var rabbitMqSettings = transportDependencyContainer + .Resolve>() + .Get(); + + Assert.Equal(settingsDirectory.Name + isolationLevel, rabbitMqSettings.VirtualHost); + } + + var username = "qwerty"; + var password = "12345678"; + + var authorizationToken = endpointDependencyContainer + .Resolve() + .GenerateToken(username, new[] { Features.Authentication }, TimeSpan.FromSeconds(60)); + + var initiatorMessage = endpointDependencyContainer + .Resolve() + .CreateGeneralMessage( + new Command(42), + typeof(Command), + new[] { new Authorization(authorizationToken) }, + null); + + var request = new AuthenticateUser(username, password); + UserAuthenticationResult? userAuthenticationResult; + + await using (transportDependencyContainer.OpenScopeAsync().ConfigureAwait(false)) + { + userAuthenticationResult = await endpointDependencyContainer + .Resolve() + .RpcRequest(request, CancellationToken.None) + .ConfigureAwait(false); + } + + output.WriteLine(userAuthenticationResult.Dump(BindingFlags.Instance | BindingFlags.Public | BindingFlags.GetProperty)); + + Assert.Equal(username, userAuthenticationResult.Username); + Assert.Empty(userAuthenticationResult.Token); + + await using (transportDependencyContainer.OpenScopeAsync().ConfigureAwait(false)) + { + var collector = transportDependencyContainer.Resolve(); + + var awaiter = Task.WhenAll( + collector.WaitUntilMessageIsNotReceived(), + collector.WaitUntilMessageIsNotReceived()); + + var integrationMessage = endpointDependencyContainer + .Resolve() + .CreateGeneralMessage( + new CreateUser(username, password), + typeof(CreateUser), + Array.Empty(), + null); + + await transportDependencyContainer + .Resolve() + .Enqueue(integrationMessage, token) + .ConfigureAwait(false); + + await awaiter.ConfigureAwait(false); + } + + await using (transportDependencyContainer.OpenScopeAsync().ConfigureAwait(false)) + { + userAuthenticationResult = await endpointDependencyContainer + .Resolve() + .RpcRequest(request, CancellationToken.None) + .ConfigureAwait(false); + } + + output.WriteLine(userAuthenticationResult.Dump(BindingFlags.Instance | BindingFlags.Public | BindingFlags.GetProperty)); + + Assert.Equal(username, userAuthenticationResult.Username); + Assert.NotEmpty(userAuthenticationResult.Token); + }; + } + } + + [Theory(Timeout = 60_000)] + [MemberData(nameof(EndpointDataAccessTestData))] + internal async Task Orm_applies_cascade_delete_strategy( + Func settingsDirectoryProducer, + TransportIdentity transportIdentity, + Func useTransport, + Func?, IEndpointBuilder> withDataAccess, + Func withEventSourcing, + IsolationLevel isolationLevel) + { + var settingsDirectory = settingsDirectoryProducer("OrmAppliesCascadeDeleteStrategy"); + + var messageTypes = new[] + { + typeof(Request), + typeof(Reply) + }; + + var messageHandlerTypes = new[] + { + typeof(AlwaysReplyRequestHandler), + typeof(ReplyHandler) + }; + + var startupActions = new[] + { + typeof(RecreatePostgreSqlDatabaseHostedServiceStartupAction) + }; + + var additionalOurTypes = messageTypes + .Concat(messageHandlerTypes) + .Concat(startupActions) + .ToArray(); + + var host = Fixture + .CreateHostBuilder() + .UseIntegrationTransport(transportIdentity, useTransport, settingsDirectory) + .UseEndpoint(TestIdentity.Endpoint10, + builder => withEventSourcing(withDataAccess(builder, options => options.ExecuteMigrations())) + .ModifyContainerOptions(options => options + .WithAdditionalOurTypes(additionalOurTypes) + .WithManualRegistrations(new IsolationLevelManualRegistration(isolationLevel))) + .BuildOptions()) + .BuildHost(settingsDirectory); + + await host + .RunTestHost(Output, TestCase, CascadeDeleteTestInternal(settingsDirectory, transportIdentity, isolationLevel)) + .ConfigureAwait(false); + + static Func CascadeDeleteTestInternal( + DirectoryInfo settingsDirectory, + TransportIdentity transportIdentity, + IsolationLevel isolationLevel) + { + return async (output, host, token) => + { + var transportDependencyContainer = host.GetIntegrationTransportDependencyContainer(transportIdentity); + var endpointDependencyContainer = host.GetEndpointDependencyContainer(TestIdentity.Endpoint10); + + var sqlDatabaseSettings = endpointDependencyContainer + .Resolve>() + .Get(); + + Assert.Equal(settingsDirectory.Name, sqlDatabaseSettings.Database); + Assert.Equal(isolationLevel, sqlDatabaseSettings.IsolationLevel); + Assert.Equal(1u, sqlDatabaseSettings.ConnectionPoolSize); + + if (transportDependencyContainer.Resolve() is RabbitMqIntegrationTransport) + { + var rabbitMqSettings = transportDependencyContainer + .Resolve>() + .Get(); + + Assert.Equal(settingsDirectory.Name + isolationLevel, rabbitMqSettings.VirtualHost); + } + + var modelProvider = endpointDependencyContainer.Resolve(); + + var databaseEntities = endpointDependencyContainer + .Resolve() + .DatabaseEntities() + .ToList(); + + Assert.Contains(typeof(InboxMessage), databaseEntities); + Assert.Contains(typeof(OutboxMessage), databaseEntities); + Assert.Contains(typeof(IntegrationMessage), databaseEntities); + Assert.Contains(typeof(IntegrationMessageHeader), databaseEntities); + + Assert.Equal(EnOnDeleteBehavior.Cascade, typeof(InboxMessage).GetProperty(nameof(InboxMessage.Message), BindingFlags.Public | BindingFlags.Instance | BindingFlags.GetProperty)?.GetRequiredAttribute().OnDeleteBehavior); + Assert.Equal(EnOnDeleteBehavior.Cascade, typeof(OutboxMessage).GetProperty(nameof(OutboxMessage.Message), BindingFlags.Public | BindingFlags.Instance | BindingFlags.GetProperty)?.GetRequiredAttribute().OnDeleteBehavior); + Assert.Equal(EnOnDeleteBehavior.Cascade, typeof(IntegrationMessageHeader).GetProperty(nameof(IntegrationMessageHeader.Message), BindingFlags.Public | BindingFlags.Instance | BindingFlags.GetProperty)?.GetRequiredAttribute().OnDeleteBehavior); + + var mtmType = modelProvider + .TablesMap[nameof(GenericEndpoint.DataAccess.Sql.Deduplication)][$"{nameof(IntegrationMessage)}_{nameof(IntegrationMessageHeader)}"] + .Type; + + Assert.Equal(EnOnDeleteBehavior.Cascade, mtmType.GetProperty(nameof(BaseMtmDatabaseEntity.Left), BindingFlags.Public | BindingFlags.Instance | BindingFlags.GetProperty)?.GetRequiredAttribute().OnDeleteBehavior); + Assert.Equal(EnOnDeleteBehavior.Cascade, mtmType.GetProperty(nameof(BaseMtmDatabaseEntity.Right), BindingFlags.Public | BindingFlags.Instance | BindingFlags.GetProperty)?.GetRequiredAttribute().OnDeleteBehavior); + + await using (transportDependencyContainer.OpenScopeAsync().ConfigureAwait(false)) + { + var collector = transportDependencyContainer.Resolve(); + + var awaiter = Task.WhenAll( + collector.WaitUntilMessageIsNotReceived(), + collector.WaitUntilMessageIsNotReceived(), + collector.WaitUntilErrorMessageIsNotReceived(message => message.HandlerType == typeof(ReplyHandler) && message.EndpointIdentity == TestIdentity.Endpoint10)); + + var integrationMessage = endpointDependencyContainer + .Resolve() + .CreateGeneralMessage( + new Request(42), + typeof(Request), + Array.Empty(), + null); + + await transportDependencyContainer + .Resolve() + .Enqueue(integrationMessage, token) + .ConfigureAwait(false); + + await awaiter.ConfigureAwait(false); + } + + await endpointDependencyContainer + .InvokeWithinTransaction(false, modelProvider, CheckRows, token) + .ConfigureAwait(false); + + await endpointDependencyContainer + .InvokeWithinTransaction(true, Delete, token) + .ConfigureAwait(false); + + await endpointDependencyContainer + .InvokeWithinTransaction(false, modelProvider, CheckEmptyRows, token) + .ConfigureAwait(false); + }; + } + + static async Task CheckRows( + IAdvancedDatabaseTransaction transaction, + IModelProvider modelProvider, + CancellationToken token) + { + Assert.True(await transaction.All().CachedExpression("75B52E36-C22C-4FAE-BA89-E67C232ED2BE").AnyAsync(token).ConfigureAwait(false)); + Assert.True(await transaction.All().CachedExpression("D50AA461-3C90-42BA-AF90-FD0E059562BA").AnyAsync(token).ConfigureAwait(false)); + Assert.True(await transaction.All().CachedExpression("C2F0D883-68FB-4887-B11E-54E2F835E552").AnyAsync(token).ConfigureAwait(false)); + Assert.True(await transaction.All().CachedExpression("CCAEB0A3-B95E-45D1-88FA-609B91F4737B").AnyAsync(token).ConfigureAwait(false)); + Assert.True(await transaction.AllMtm(modelProvider, message => message.Headers).Cast().CachedExpression("04494178-C124-4BF1-8841-DEA3427A3E99").AnyAsync(token).ConfigureAwait(false)); + + var rowsCount = await transaction.All().CachedExpression("37A34847-5A14-4D4E-928A-75683BBB1514").CountAsync(token).ConfigureAwait(false); + + Assert.Equal(3, rowsCount); + } + + static async Task Delete( + IAdvancedDatabaseTransaction transaction, + CancellationToken token) + { + var affectedRowsCount = await transaction + .Delete() + .Where(_ => true) + .CachedExpression("4C8F330F-9142-486C-90BE-6F76B262487A") + .Invoke(token) + .ConfigureAwait(false); + + Assert.Equal(3, affectedRowsCount); + } + + static async Task CheckEmptyRows( + IAdvancedDatabaseTransaction transaction, + IModelProvider modelProvider, + CancellationToken token) + { + Assert.False(await transaction.All().CachedExpression("75B52E36-C22C-4FAE-BA89-E67C232ED2BE").AnyAsync(token).ConfigureAwait(false)); + Assert.False(await transaction.All().CachedExpression("D50AA461-3C90-42BA-AF90-FD0E059562BA").AnyAsync(token).ConfigureAwait(false)); + Assert.False(await transaction.All().CachedExpression("C2F0D883-68FB-4887-B11E-54E2F835E552").AnyAsync(token).ConfigureAwait(false)); + Assert.False(await transaction.All().CachedExpression("CCAEB0A3-B95E-45D1-88FA-609B91F4737B").AnyAsync(token).ConfigureAwait(false)); + Assert.False(await transaction.AllMtm(modelProvider, message => message.Headers).Cast().CachedExpression("04494178-C124-4BF1-8841-DEA3427A3E99").AnyAsync(token).ConfigureAwait(false)); + } + } + } +} \ No newline at end of file diff --git a/tests/Tests/GenericHost.Test/DatabaseEntities/ComplexDatabaseEntity.cs b/tests/Tests/GenericHost.Test/DatabaseEntities/ComplexDatabaseEntity.cs new file mode 100644 index 00000000..cc5c1186 --- /dev/null +++ b/tests/Tests/GenericHost.Test/DatabaseEntities/ComplexDatabaseEntity.cs @@ -0,0 +1,161 @@ +namespace SpaceEngineers.Core.GenericHost.Test.DatabaseEntities +{ + using System; + using System.Diagnostics.CodeAnalysis; + using DataAccess.Orm.Sql.Model; + using DataAccess.Orm.Sql.Model.Attributes; + using GenericEndpoint.Contract.Abstractions; + using Relations; + + [SuppressMessage("Analysis", "SA1011", Justification = "space between square brackets and nullable symbol")] + [Schema(nameof(GenericHost) + nameof(Test))] + internal record ComplexDatabaseEntity : BaseDatabaseEntity + { + public ComplexDatabaseEntity(Guid primaryKey) + : base(primaryKey) + { + } + + public double Number { get; set; } + + public double? NullableNumber { get; set; } + + public Guid Identifier { get; set; } + + public Guid? NullableIdentifier { get; set; } + + public bool Boolean { get; set; } + + public bool? NullableBoolean { get; set; } + + public DateTime DateTime { get; set; } + + public DateTime? NullableDateTime { get; set; } + + public TimeSpan TimeSpan { get; set; } + + public TimeSpan? NullableTimeSpan { get; set; } + + public DateOnly DateOnly { get; set; } + + public DateOnly? NullableDateOnly { get; set; } + + public TimeOnly TimeOnly { get; set; } + + public TimeOnly? NullableTimeOnly { get; set; } + + public byte[] ByteArray { get; set; } = default!; + + public string String { get; set; } = default!; + + public string? NullableString { get; set; } + + public EnEnum Enum { get; set; } + + public EnEnum? NullableEnum { get; set; } + + public EnEnumFlags EnumFlags { get; set; } + + public EnEnumFlags? NullableEnumFlags { get; set; } + + public EnEnum[] EnumArray { get; set; } = Array.Empty(); + + public EnEnum?[] NullableEnumArray { get; set; } = Array.Empty(); + + public string[] StringArray { get; set; } = Array.Empty(); + + public string?[] NullableStringArray { get; set; } = Array.Empty(); + + public DateTime[] DateTimeArray { get; set; } = Array.Empty(); + + public DateTime?[] NullableDateTimeArray { get; set; } = Array.Empty(); + + [JsonColumn] + public IIntegrationMessage Json { get; set; } = default!; + + [JsonColumn] + public IIntegrationMessage? NullableJson { get; set; } + + [ForeignKey(EnOnDeleteBehavior.NoAction)] + public Blog Relation { get; set; } = default!; + + [ForeignKey(EnOnDeleteBehavior.NoAction)] + public Blog? NullableRelation { get; set; } + + public static ComplexDatabaseEntity Generate(IIntegrationMessage json, Blog relation) + { + return new ComplexDatabaseEntity(Guid.NewGuid()) + { + Number = 42, + NullableNumber = 42, + Identifier = Guid.NewGuid(), + NullableIdentifier = Guid.NewGuid(), + Boolean = true, + NullableBoolean = true, + DateTime = DateTime.Today, + NullableDateTime = DateTime.Today, + TimeSpan = TimeSpan.FromHours(3), + NullableTimeSpan = TimeSpan.FromHours(3), + DateOnly = DateOnly.FromDateTime(DateTime.Today), + NullableDateOnly = DateOnly.FromDateTime(DateTime.Today), + TimeOnly = TimeOnly.FromTimeSpan(TimeSpan.FromHours(3)), + NullableTimeOnly = TimeOnly.FromTimeSpan(TimeSpan.FromHours(3)), + ByteArray = new byte[] { 1, 2, 3 }, + String = "SomeString", + NullableString = "SomeNullableString", + Enum = EnEnum.Three, + NullableEnum = EnEnum.Three, + EnumFlags = EnEnumFlags.A | EnEnumFlags.B, + NullableEnumFlags = EnEnumFlags.A | EnEnumFlags.B, + EnumArray = new[] { EnEnum.One, EnEnum.Two, EnEnum.Three }, + NullableEnumArray = new EnEnum?[] { EnEnum.One, EnEnum.Two, EnEnum.Three }, + StringArray = new[] { "SomeString", "AnotherString" }, + NullableStringArray = new[] { "SomeString", "AnotherString" }, + DateTimeArray = new[] { DateTime.MaxValue, DateTime.MaxValue }, + NullableDateTimeArray = new DateTime?[] { DateTime.MaxValue, DateTime.MaxValue }, + Json = json, + NullableJson = json, + Relation = relation, + NullableRelation = relation + }; + } + + public static ComplexDatabaseEntity GenerateWithNulls(IIntegrationMessage json, Blog relation) + { + return new ComplexDatabaseEntity(Guid.NewGuid()) + { + Number = 42, + NullableNumber = null, + Identifier = Guid.NewGuid(), + NullableIdentifier = null, + Boolean = true, + NullableBoolean = null, + DateTime = DateTime.Today, + NullableDateTime = null, + TimeSpan = TimeSpan.FromHours(3), + NullableTimeSpan = null, + DateOnly = DateOnly.FromDateTime(DateTime.Today), + NullableDateOnly = null, + TimeOnly = TimeOnly.FromTimeSpan(TimeSpan.FromHours(3)), + NullableTimeOnly = null, + ByteArray = new byte[] { 1, 2, 3 }, + String = "SomeString", + NullableString = null, + Enum = EnEnum.Three, + NullableEnum = null, + EnumFlags = EnEnumFlags.A | EnEnumFlags.B, + NullableEnumFlags = null, + EnumArray = new[] { EnEnum.One, EnEnum.Two, EnEnum.Three }, + NullableEnumArray = new EnEnum?[] { EnEnum.One, null, EnEnum.Three }, + StringArray = new[] { "SomeString", "AnotherString" }, + NullableStringArray = new[] { "SomeString", null, "AnotherString" }, + DateTimeArray = new[] { DateTime.MaxValue, DateTime.MaxValue }, + NullableDateTimeArray = new DateTime?[] { DateTime.MaxValue, null, DateTime.MaxValue }, + Json = json, + NullableJson = null, + Relation = relation, + NullableRelation = null + }; + } + } +} \ No newline at end of file diff --git a/tests/Tests/GenericHost.Test/DatabaseEntities/ComposedJsonObject.cs b/tests/Tests/GenericHost.Test/DatabaseEntities/ComposedJsonObject.cs new file mode 100644 index 00000000..0c2025dc --- /dev/null +++ b/tests/Tests/GenericHost.Test/DatabaseEntities/ComposedJsonObject.cs @@ -0,0 +1,17 @@ +namespace SpaceEngineers.Core.GenericHost.Test.DatabaseEntities +{ + using System; + + internal class ComposedJsonObject + { + public ComposedJsonObject(string value, Guid aggregateId) + { + Value = value; + AggregateId = aggregateId; + } + + public string Value { get; init; } + + public Guid AggregateId { get; init; } + } +} \ No newline at end of file diff --git a/tests/Tests/GenericHost.Test/DatabaseEntities/DatabaseEntity.cs b/tests/Tests/GenericHost.Test/DatabaseEntities/DatabaseEntity.cs new file mode 100644 index 00000000..faf3a139 --- /dev/null +++ b/tests/Tests/GenericHost.Test/DatabaseEntities/DatabaseEntity.cs @@ -0,0 +1,49 @@ +namespace SpaceEngineers.Core.GenericHost.Test.DatabaseEntities +{ + using System; + using System.Diagnostics.CodeAnalysis; + using DataAccess.Orm.Sql.Model; + using DataAccess.Orm.Sql.Model.Attributes; + + [SuppressMessage("Analysis", "SA1649", Justification = "StyleCop analyzer error")] + [Schema(nameof(GenericHost) + nameof(Test))] + [Index(nameof(StringField), IncludedColumns = new[] { nameof(IntField) }, Predicate = $@"""{nameof(BooleanField)}""")] + internal record DatabaseEntity : BaseDatabaseEntity + { + public DatabaseEntity( + Guid primaryKey, + bool booleanField, + string stringField, + string? nullableStringField, + int intField, + EnEnum @enum) + : base(primaryKey) + { + BooleanField = booleanField; + StringField = stringField; + NullableStringField = nullableStringField; + IntField = intField; + Enum = @enum; + } + + public bool BooleanField { get; set; } + + public string StringField { get; set; } + + public string? NullableStringField { get; set; } + + public int IntField { get; set; } + + public EnEnum Enum { get; set; } + + public static DatabaseEntity Generate() + { + return Generate(Guid.NewGuid()); + } + + public static DatabaseEntity Generate(Guid primaryKey) + { + return new DatabaseEntity(primaryKey, true, "SomeString", "SomeNullableString", 42, EnEnum.Three); + } + } +} \ No newline at end of file diff --git a/tests/Tests/GenericHost.Test/DatabaseEntities/EnEnum.cs b/tests/Tests/GenericHost.Test/DatabaseEntities/EnEnum.cs new file mode 100644 index 00000000..4cf8a59c --- /dev/null +++ b/tests/Tests/GenericHost.Test/DatabaseEntities/EnEnum.cs @@ -0,0 +1,20 @@ +namespace SpaceEngineers.Core.GenericHost.Test.DatabaseEntities +{ + internal enum EnEnum + { + /// + /// One + /// + One = 1, + + /// + /// Two + /// + Two = 2, + + /// + /// Three + /// + Three = 4 + } +} \ No newline at end of file diff --git a/tests/Tests/GenericHost.Test/DatabaseEntities/EnEnumFlags.cs b/tests/Tests/GenericHost.Test/DatabaseEntities/EnEnumFlags.cs new file mode 100644 index 00000000..d568acca --- /dev/null +++ b/tests/Tests/GenericHost.Test/DatabaseEntities/EnEnumFlags.cs @@ -0,0 +1,23 @@ +namespace SpaceEngineers.Core.GenericHost.Test.DatabaseEntities +{ + using System; + + [Flags] + internal enum EnEnumFlags + { + /// + /// A + /// + A = 1 << 0, + + /// + /// B + /// + B = 1 << 1, + + /// + /// c + /// + C = 1 << 2 + } +} \ No newline at end of file diff --git a/tests/Tests/GenericHost.Test/DatabaseEntities/Relations/Blog.cs b/tests/Tests/GenericHost.Test/DatabaseEntities/Relations/Blog.cs new file mode 100644 index 00000000..90774776 --- /dev/null +++ b/tests/Tests/GenericHost.Test/DatabaseEntities/Relations/Blog.cs @@ -0,0 +1,25 @@ +namespace SpaceEngineers.Core.GenericHost.Test.DatabaseEntities.Relations +{ + using System; + using System.Collections.Generic; + using DataAccess.Orm.Sql.Model; + using DataAccess.Orm.Sql.Model.Attributes; + + [Schema(nameof(GenericHost) + nameof(Test))] + internal record Blog : BaseDatabaseEntity + { + public Blog( + Guid primaryKey, + string theme, + IReadOnlyCollection posts) + : base(primaryKey) + { + Theme = theme; + Posts = posts; + } + + public string Theme { get; set; } + + public IReadOnlyCollection Posts { get; set; } + } +} \ No newline at end of file diff --git a/tests/Tests/GenericHost.Test/DatabaseEntities/Relations/Community.cs b/tests/Tests/GenericHost.Test/DatabaseEntities/Relations/Community.cs new file mode 100644 index 00000000..d45ecc55 --- /dev/null +++ b/tests/Tests/GenericHost.Test/DatabaseEntities/Relations/Community.cs @@ -0,0 +1,25 @@ +namespace SpaceEngineers.Core.GenericHost.Test.DatabaseEntities.Relations +{ + using System; + using System.Collections.Generic; + using DataAccess.Orm.Sql.Model; + using DataAccess.Orm.Sql.Model.Attributes; + + [Schema(nameof(GenericHost) + nameof(Test))] + internal record Community : BaseDatabaseEntity + { + public Community( + Guid primaryKey, + string name, + IReadOnlyCollection participants) + : base(primaryKey) + { + Name = name; + Participants = participants; + } + + public string Name { get; set; } + + public IReadOnlyCollection Participants { get; set; } + } +} \ No newline at end of file diff --git a/tests/Tests/GenericHost.Test/DatabaseEntities/Relations/Participant.cs b/tests/Tests/GenericHost.Test/DatabaseEntities/Relations/Participant.cs new file mode 100644 index 00000000..4944f86d --- /dev/null +++ b/tests/Tests/GenericHost.Test/DatabaseEntities/Relations/Participant.cs @@ -0,0 +1,25 @@ +namespace SpaceEngineers.Core.GenericHost.Test.DatabaseEntities.Relations +{ + using System; + using System.Collections.Generic; + using DataAccess.Orm.Sql.Model; + using DataAccess.Orm.Sql.Model.Attributes; + + [Schema(nameof(GenericHost) + nameof(Test))] + internal record Participant : BaseDatabaseEntity + { + public Participant( + Guid primaryKey, + string name, + IReadOnlyCollection communities) + : base(primaryKey) + { + Name = name; + Communities = communities; + } + + public string Name { get; set; } + + public IReadOnlyCollection Communities { get; set; } + } +} \ No newline at end of file diff --git a/tests/Tests/GenericHost.Test/DatabaseEntities/Relations/Post.cs b/tests/Tests/GenericHost.Test/DatabaseEntities/Relations/Post.cs new file mode 100644 index 00000000..fac917d5 --- /dev/null +++ b/tests/Tests/GenericHost.Test/DatabaseEntities/Relations/Post.cs @@ -0,0 +1,34 @@ +namespace SpaceEngineers.Core.GenericHost.Test.DatabaseEntities.Relations +{ + using System; + using DataAccess.Orm.Sql.Model; + using DataAccess.Orm.Sql.Model.Attributes; + + [Schema(nameof(GenericHost) + nameof(Test))] + internal record Post : BaseDatabaseEntity + { + public Post( + Guid primaryKey, + Blog blog, + User user, + DateTime dateTime, + string text) + : base(primaryKey) + { + Blog = blog; + User = user; + DateTime = dateTime; + Text = text; + } + + [ForeignKey(EnOnDeleteBehavior.Cascade)] + public Blog Blog { get; set; } + + [ForeignKey(EnOnDeleteBehavior.Restrict)] + public User User { get; set; } + + public DateTime DateTime { get; set; } + + public string Text { get; set; } + } +} \ No newline at end of file diff --git a/tests/Tests/GenericHost.Test/DatabaseEntities/Relations/User.cs b/tests/Tests/GenericHost.Test/DatabaseEntities/Relations/User.cs new file mode 100644 index 00000000..a89b8b57 --- /dev/null +++ b/tests/Tests/GenericHost.Test/DatabaseEntities/Relations/User.cs @@ -0,0 +1,18 @@ +namespace SpaceEngineers.Core.GenericHost.Test.DatabaseEntities.Relations +{ + using System; + using DataAccess.Orm.Sql.Model; + using DataAccess.Orm.Sql.Model.Attributes; + + [Schema(nameof(GenericHost) + nameof(Test))] + internal record User : BaseDatabaseEntity + { + public User(Guid primaryKey, string nickname) + : base(primaryKey) + { + Nickname = nickname; + } + + public string Nickname { get; set; } + } +} \ No newline at end of file diff --git a/tests/Tests/GenericHost.Test/Extensions/GenericHostTestExtensions.cs b/tests/Tests/GenericHost.Test/Extensions/GenericHostTestExtensions.cs new file mode 100644 index 00000000..ac53582f --- /dev/null +++ b/tests/Tests/GenericHost.Test/Extensions/GenericHostTestExtensions.cs @@ -0,0 +1,65 @@ +namespace SpaceEngineers.Core.GenericHost.Test.Extensions +{ + using System; + using System.IO; + using System.Threading; + using System.Threading.Tasks; + using IntegrationTransport.Api; + using Microsoft.Extensions.DependencyInjection; + using Microsoft.Extensions.Hosting; + using Xunit.Abstractions; + using Xunit.Sdk; + + internal static class GenericHostTestExtensions + { + public static IHostBuilder UseIntegrationTransport( + this IHostBuilder hostBuilder, + TransportIdentity transportIdentity, + Func useTransport) + { + return useTransport(hostBuilder, transportIdentity); + } + + public static IHostBuilder UseIntegrationTransport( + this IHostBuilder hostBuilder, + TransportIdentity transportIdentity, + Func useTransport, + DirectoryInfo settingsDirectory) + { + return useTransport(hostBuilder, transportIdentity, settingsDirectory); + } + + internal static async Task RunTestHost( + this IHost host, + ITestOutputHelper output, + IXunitTestCase testCase, + Func producer) + { + using (host) + using (var cts = new CancellationTokenSource(TimeSpan.FromMilliseconds(testCase.Timeout))) + { + await host.StartAsync(cts.Token).ConfigureAwait(false); + + var hostShutdown = host.WaitForShutdownAsync(cts.Token); + + var awaiter = Task.WhenAny(producer(output, host, cts.Token), hostShutdown); + + var result = await awaiter.ConfigureAwait(false); + + if (hostShutdown == result) + { + throw new InvalidOperationException("Host was unexpectedly stopped"); + } + + await result.ConfigureAwait(false); + + host + .Services + .GetRequiredService() + .StopApplication(); + + await hostShutdown.ConfigureAwait(false); + } + } + } +} \ No newline at end of file diff --git a/tests/Tests/GenericHost.Test/GenericHost.Test.csproj b/tests/Tests/GenericHost.Test/GenericHost.Test.csproj new file mode 100644 index 00000000..4685770a --- /dev/null +++ b/tests/Tests/GenericHost.Test/GenericHost.Test.csproj @@ -0,0 +1,43 @@ + + + + net7.0 + SpaceEngineers.Core.GenericHost.Test + SpaceEngineers.Core.GenericHost.Test + false + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + + + + + + + + + diff --git a/tests/Tests/GenericHost.Test/HostBuilderTests.cs b/tests/Tests/GenericHost.Test/HostBuilderTests.cs new file mode 100644 index 00000000..579ca024 --- /dev/null +++ b/tests/Tests/GenericHost.Test/HostBuilderTests.cs @@ -0,0 +1,723 @@ +namespace SpaceEngineers.Core.GenericHost.Test +{ + using System; + using System.Collections.Generic; + using System.Diagnostics.CodeAnalysis; + using System.IO; + using System.Linq; + using System.Threading; + using System.Threading.Tasks; + using Basics; + using CompositionRoot; + using CompositionRoot.Exceptions; + using CompositionRoot.Extensions; + using Core.Test.Api; + using Core.Test.Api.ClassFixtures; + using CrossCuttingConcerns.Settings; + using Extensions; + using GenericEndpoint.Api.Abstractions; + using GenericEndpoint.Authorization; + using GenericEndpoint.Authorization.Host; + using GenericEndpoint.Contract; + using GenericEndpoint.DataAccess.Sql.Host.BackgroundWorkers; + using GenericEndpoint.DataAccess.Sql.Host.StartupActions; + using GenericEndpoint.DataAccess.Sql.Postgres.Host; + using GenericEndpoint.DataAccess.Sql.Postgres.Host.StartupActions; + using GenericEndpoint.EventSourcing.Host; + using GenericEndpoint.EventSourcing.Host.StartupActions; + using GenericEndpoint.Host; + using GenericEndpoint.Host.StartupActions; + using GenericEndpoint.Messaging; + using GenericEndpoint.Pipeline; + using GenericEndpoint.Telemetry; + using GenericEndpoint.Telemetry.Host; + using GenericEndpoint.Web.Host; + using IntegrationTransport.Api; + using IntegrationTransport.Api.Abstractions; + using IntegrationTransport.Host; + using IntegrationTransport.Host.BackgroundWorkers; + using IntegrationTransport.RabbitMQ; + using IntegrationTransport.RabbitMQ.Settings; + using MessageHandlers; + using Messages; + using Microsoft.Extensions.DependencyInjection; + using Microsoft.Extensions.Hosting; + using Xunit; + using Xunit.Abstractions; + using Identity = IntegrationTransport.InMemory.Identity; + using IntegrationMessage = GenericEndpoint.Messaging.IntegrationMessage; + + /// + /// HostBuilder tests + /// + [SuppressMessage("Analysis", "CA1506", Justification = "application composition root")] + public class HostBuilderTests : TestBase + { + /// .cctor + /// ITestOutputHelper + /// TestFixture + public HostBuilderTests(ITestOutputHelper output, TestFixture fixture) + : base(output, fixture) + { + } + + /// + /// Test cases for HostBuilder tests + /// + /// Test cases + public static IEnumerable HostBuilderTestData() + { + var projectFileDirectory = SolutionExtensions.ProjectFile().Directory + ?? throw new InvalidOperationException("Project directory wasn't found"); + + var settingsDirectory = projectFileDirectory + .StepInto("Settings") + .StepInto(nameof(HostBuilderTests)); + + var inMemoryIntegrationTransportIdentity = Identity.TransportIdentity(); + + var useInMemoryIntegrationTransport = new Func( + static (hostBuilder, transportIdentity) => hostBuilder.UseInMemoryIntegrationTransport(transportIdentity)); + + var rabbitMqIntegrationTransportIdentity = IntegrationTransport.RabbitMQ.Identity.TransportIdentity(); + + var useRabbitMqIntegrationTransport = new Func( + static (hostBuilder, transportIdentity) => hostBuilder.UseRabbitMqIntegrationTransport(transportIdentity)); + + var integrationTransportProviders = new[] + { + (inMemoryIntegrationTransportIdentity, useInMemoryIntegrationTransport), + (rabbitMqIntegrationTransportIdentity, useRabbitMqIntegrationTransport) + }; + + return integrationTransportProviders + .Select(transport => + { + var (transportIdentity, useTransport) = transport; + + return new object[] + { + settingsDirectory, + transportIdentity, + useTransport + }; + }); + } + + [Theory(Timeout = 60_000)] + [MemberData(nameof(HostBuilderTestData))] + internal void HostBuilder_utilizes_same_integration_transport_instance_between_its_endpoints( + DirectoryInfo settingsDirectory, + TransportIdentity transportIdentity, + Func useTransport) + { + var host = Fixture + .CreateHostBuilder() + .UseIntegrationTransport(transportIdentity, useTransport) + .UseEndpoint( + TestIdentity.Endpoint10, + builder => builder.BuildOptions()) + .UseEndpoint( + TestIdentity.Endpoint20, + builder => builder.BuildOptions()) + .BuildHost(settingsDirectory); + + var gatewayHost = Fixture + .CreateHostBuilder() + .UseIntegrationTransport(transportIdentity, useTransport) + .BuildHost(settingsDirectory); + + var gatewayTransport = gatewayHost.GetIntegrationTransportDependencyContainer(transportIdentity).Resolve(); + var hostTransport = host.GetIntegrationTransportDependencyContainer(transportIdentity).Resolve(); + var endpoint10Transport = host.GetEndpointDependencyContainer(TestIdentity.Endpoint10).Resolve(); + var endpoint20Transport = host.GetEndpointDependencyContainer(TestIdentity.Endpoint20).Resolve(); + + Assert.NotSame(hostTransport, gatewayTransport); + Assert.Same(hostTransport, endpoint10Transport); + Assert.Same(hostTransport, endpoint20Transport); + } + + [Theory(Timeout = 60_000)] + [MemberData(nameof(HostBuilderTestData))] + internal void Assert_host_and_its_endpoints_configuration( + DirectoryInfo settingsDirectory, + TransportIdentity transportIdentity, + Func useTransport) + { + var messageTypes = new[] + { + typeof(BaseEvent), + typeof(InheritedEvent), + typeof(Command), + typeof(OpenGenericHandlerCommand), + typeof(Request), + typeof(Reply) + }; + + var messageHandlerTypes = new[] + { + typeof(BaseEventHandler), + typeof(InheritedEventHandler), + typeof(CommandHandler), + typeof(OpenGenericCommandHandler<>), + typeof(AlwaysReplyRequestHandler), + typeof(ReplyHandler) + }; + + var additionalOurTypes = messageTypes + .Concat(messageHandlerTypes) + .ToArray(); + + var host = Fixture + .CreateHostBuilder() + .UseIntegrationTransport(transportIdentity, useTransport) + .UseEndpoint(TestIdentity.Endpoint10, + builder => builder + .WithPostgreSqlDataAccess(options => options + .ExecuteMigrations()) + .WithSqlEventSourcing() + .WithJwtAuthentication(builder.Context.Configuration) + .WithAuthorization() + .WithOpenTelemetry() + .ModifyContainerOptions(options => options + .WithAdditionalOurTypes(additionalOurTypes)) + .BuildOptions()) + .UseOpenTelemetry() + .BuildHost(settingsDirectory); + + using (host) + { + CheckHost(host); + CheckTransport(host, transportIdentity, Output.WriteLine); + CheckEndpoint(host, TestIdentity.Endpoint10, Output.WriteLine); + } + + static void CheckHost(IHost host) + { + var hostedServices = host + .Services + .GetServices() + .OfType() + .ToArray(); + + Assert.Equal(2, hostedServices.Length); + } + + static void CheckTransport(IHost host, TransportIdentity transportIdentity, Action log) + { + log($"Endpoint: {transportIdentity}"); + + IDependencyContainer integrationTransportDependencyContainer = host.GetIntegrationTransportDependencyContainer(transportIdentity); + + // IHostedServiceStartupAction + { + var expectedHostedServiceStartupActions = Array + .Empty() + .OrderByDependencies() + .ThenBy(type => type.Name) + .ToList(); + + var actualHostedServiceStartupActions = integrationTransportDependencyContainer + .ResolveCollection() + .Select(startup => startup.GetType()) + .OrderByDependencies() + .ThenBy(type => type.Name) + .ToList(); + + Assert.Equal(expectedHostedServiceStartupActions, actualHostedServiceStartupActions); + } + + // IHostedServiceBackgroundWorker + { + var expectedHostedServiceBackgroundWorkers = new[] + { + typeof(IntegrationTransportHostedServiceBackgroundWorker) + } + .OrderByDependencies() + .ThenBy(type => type.Name) + .ToList(); + + var actualHostedServiceBackgroundWorkers = integrationTransportDependencyContainer + .ResolveCollection() + .Select(startup => startup.GetType()) + .OrderByDependencies() + .ThenBy(type => type.Name) + .ToList(); + + Assert.Equal(expectedHostedServiceBackgroundWorkers, actualHostedServiceBackgroundWorkers); + } + + // IHostedServiceObject + { + var expectedHostedServiceBackgroundWorkers = new[] + { + typeof(IntegrationTransportHostedServiceBackgroundWorker) + } + .OrderByDependencies() + .ThenBy(type => type.Name) + .ToList(); + + var actualHostedServiceObjects = integrationTransportDependencyContainer + .ResolveCollection() + .Select(startup => startup.GetType()) + .OrderByDependencies() + .ThenBy(type => type.Name) + .ToList(); + + Assert.Equal(expectedHostedServiceBackgroundWorkers, actualHostedServiceObjects); + } + + // IntegrationContext + { + var integrationMessage = new IntegrationMessage(new Command(0), typeof(Command)); + + Assert.Throws(() => integrationTransportDependencyContainer.Resolve()); + Assert.Throws(() => integrationTransportDependencyContainer.Resolve(integrationMessage)); + Assert.Throws(() => integrationTransportDependencyContainer.Resolve()); + } + } + + static void CheckEndpoint(IHost host, EndpointIdentity endpointIdentity, Action log) + { + log($"Endpoint: {endpointIdentity}"); + + var endpointDependencyContainer = host.GetEndpointDependencyContainer(endpointIdentity); + + // IHostedServiceStartupAction + { + var expectedHostedServiceStartupActions = new[] + { + typeof(EventSourcingHostedServiceStartupAction), + typeof(InboxInvalidationHostedServiceStartupAction), + typeof(MessagingHostedServiceStartupAction), + typeof(UpgradeDatabaseHostedServiceStartupAction), + typeof(ReloadNpgsqlTypesHostedServiceStartupAction), + typeof(GenericEndpointHostedServiceStartupAction) + } + .OrderByDependencies() + .ThenBy(type => type.Name) + .ToList(); + + var actualHostedServiceStartupActions = endpointDependencyContainer + .ResolveCollection() + .Select(startup => startup.GetType()) + .OrderByDependencies() + .ThenBy(type => type.Name) + .ToList(); + + Assert.Equal(expectedHostedServiceStartupActions, actualHostedServiceStartupActions); + } + + // IHostedServiceBackgroundWorker + { + var expectedHostedServiceBackgroundWorkers = new[] + { + typeof(GenericEndpointDataAccessHostedServiceBackgroundWorker) + } + .OrderByDependencies() + .ThenBy(type => type.Name) + .ToList(); + + var actualHostedServiceBackgroundWorkers = endpointDependencyContainer + .ResolveCollection() + .Select(startup => startup.GetType()) + .OrderByDependencies() + .ThenBy(type => type.Name) + .ToList(); + + Assert.Equal(expectedHostedServiceBackgroundWorkers, actualHostedServiceBackgroundWorkers); + } + + // IHostedServiceObject + { + var expectedHostedServiceBackgroundWorkers = new[] + { + typeof(EventSourcingHostedServiceStartupAction), + typeof(InboxInvalidationHostedServiceStartupAction), + typeof(MessagingHostedServiceStartupAction), + typeof(UpgradeDatabaseHostedServiceStartupAction), + typeof(ReloadNpgsqlTypesHostedServiceStartupAction), + typeof(GenericEndpointHostedServiceStartupAction), + typeof(GenericEndpointDataAccessHostedServiceBackgroundWorker) + } + .OrderByDependencies() + .ThenBy(type => type.Name) + .ToList(); + + var actualHostedServiceObjects = endpointDependencyContainer + .ResolveCollection() + .Select(startup => startup.GetType()) + .OrderByDependencies() + .ThenBy(type => type.Name) + .ToList(); + + Assert.Equal(expectedHostedServiceBackgroundWorkers, actualHostedServiceObjects); + } + + // IntegrationContext + { + var integrationMessage = new IntegrationMessage(new Command(0), typeof(Command)); + + Assert.Throws(() => endpointDependencyContainer.Resolve()); + Assert.Throws(() => endpointDependencyContainer.Resolve(integrationMessage)); + Assert.Throws(() => endpointDependencyContainer.Resolve()); + } + + using (endpointDependencyContainer.OpenScope()) + { + // IIntegrationContext + { + var expectedContexts = new[] + { + typeof(AdvancedIntegrationContext) + }; + + var integrationMessage = new IntegrationMessage(new Command(0), typeof(Command)); + + Assert.Throws(() => endpointDependencyContainer.Resolve()); + var advancedIntegrationContext = endpointDependencyContainer.Resolve(integrationMessage); + + var actualAdvancedIntegrationContexts = advancedIntegrationContext + .FlattenDecoratedObject(obj => obj.GetType()) + .ShowTypes(nameof(IAdvancedIntegrationContext), log) + .ToList(); + + Assert.Equal(expectedContexts, actualAdvancedIntegrationContexts); + + var integrationContext = endpointDependencyContainer.Resolve(); + + var actualIntegrationContexts = integrationContext + .FlattenDecoratedObject(obj => obj.GetType()) + .ShowTypes(nameof(IIntegrationContext), log) + .ToList(); + + Assert.Equal(expectedContexts, actualIntegrationContexts); + } + + // IMessagesCollector + { + var expectedMessagesCollector = new[] + { + typeof(MessagesCollector) + }; + + var messagesCollector = endpointDependencyContainer.Resolve(); + + var actualMessagesCollector = messagesCollector + .FlattenDecoratedObject(obj => obj.GetType()) + .ShowTypes(nameof(IMessagesCollector), log) + .ToList(); + + Assert.Equal(expectedMessagesCollector, actualMessagesCollector); + } + + // IMessageHandlerMiddleware + { + var expectedMiddlewares = new[] + { + typeof(TracingMiddleware), + typeof(ErrorHandlingMiddleware), + typeof(AuthorizationMiddleware), + typeof(UnitOfWorkMiddleware), + typeof(HandledByEndpointMiddleware), + typeof(RequestReplyMiddleware) + }; + + var actualMiddlewares = endpointDependencyContainer + .ResolveCollection() + .Select(middleware => middleware.GetType()) + .ShowTypes(nameof(IMessageHandlerMiddleware), log) + .ToList(); + + Assert.Equal(expectedMiddlewares, actualMiddlewares); + } + + // IIntegrationMessage + { + var integrationTypeProvider = endpointDependencyContainer.Resolve(); + + var expectedIntegrationMessageTypes = new[] + { + typeof(BaseEvent), + typeof(InheritedEvent), + typeof(Command), + typeof(OpenGenericHandlerCommand), + typeof(Request), + typeof(Reply) + } + .OrderBy(type => type.Name) + .ToList(); + + var actualIntegrationMessageTypes = integrationTypeProvider + .IntegrationMessageTypes() + .ShowTypes(nameof(IIntegrationTypeProvider.IntegrationMessageTypes), log) + .OrderBy(type => type.Name) + .ToList(); + + Assert.Equal(expectedIntegrationMessageTypes, actualIntegrationMessageTypes); + } + + // IIntegrationCommand + { + var integrationTypeProvider = endpointDependencyContainer.Resolve(); + + var expectedCommands = new[] + { + typeof(Command), + typeof(OpenGenericHandlerCommand) + } + .OrderBy(type => type.Name) + .ToList(); + + var actualCommands = integrationTypeProvider + .EndpointCommands() + .ShowTypes(nameof(IIntegrationTypeProvider.EndpointCommands), log) + .OrderBy(type => type.Name) + .ToList(); + + Assert.Equal(expectedCommands, actualCommands); + } + + // IIntegrationRequest + { + var integrationTypeProvider = endpointDependencyContainer.Resolve(); + + var expectedRequests = new[] + { + typeof(Request) + } + .OrderBy(type => type.Name) + .ToList(); + + var actualRequests = integrationTypeProvider + .EndpointRequests() + .ShowTypes(nameof(IIntegrationTypeProvider.EndpointRequests), log) + .OrderBy(type => type.Name) + .ToList(); + + Assert.Equal(expectedRequests, actualRequests); + } + + // IIntegrationReply + { + var integrationTypeProvider = endpointDependencyContainer.Resolve(); + + var expectedReplies = new[] + { + typeof(Reply) + } + .OrderBy(type => type.Name) + .ToList(); + + var actualReplies = integrationTypeProvider + .RepliesSubscriptions() + .ShowTypes(nameof(IIntegrationTypeProvider.RepliesSubscriptions), log) + .OrderBy(type => type.Name) + .ToList(); + + Assert.Equal(expectedReplies, actualReplies); + } + + // IIntegrationEvent + { + var integrationTypeProvider = endpointDependencyContainer.Resolve(); + + var expectedEvents = new[] + { + typeof(BaseEvent), + typeof(InheritedEvent) + } + .OrderBy(type => type.Name) + .ToList(); + + var actualEvents = integrationTypeProvider + .EventsSubscriptions() + .ShowTypes(nameof(IIntegrationTypeProvider.EventsSubscriptions), log) + .OrderBy(type => type.Name) + .ToList(); + + Assert.Equal(expectedEvents, actualEvents); + } + + // IIntegrationMessageHeaderProvider + { + var expectedIntegrationMessageHeaderProvider = new[] + { + typeof(ConversationIdProvider), + typeof(MessageInitiatorProvider), + typeof(MessageOriginProvider), + typeof(ReplyToProvider), + typeof(TraceContextPropagationProvider), + typeof(UserScopeProvider), + typeof(AuthorizationHeaderProvider), + typeof(AnonymousUserScopeProvider) + } + .ToList(); + + var actualIntegrationMessageHeaderProvider = endpointDependencyContainer + .ResolveCollection() + .Select(obj => obj.GetType()) + .ShowTypes(nameof(IIntegrationMessageHeaderProvider), log) + .ToList(); + + Assert.Equal(expectedIntegrationMessageHeaderProvider, actualIntegrationMessageHeaderProvider); + } + + // IMessageHandler + { + Assert.Equal(typeof(BaseEventHandler), endpointDependencyContainer.Resolve>().GetType()); + Assert.Equal(typeof(InheritedEventHandler), endpointDependencyContainer.Resolve>().GetType()); + Assert.Equal(typeof(CommandHandler), endpointDependencyContainer.Resolve>().GetType()); + Assert.Equal(typeof(OpenGenericCommandHandler), endpointDependencyContainer.Resolve>().GetType()); + Assert.Equal(typeof(AlwaysReplyRequestHandler), endpointDependencyContainer.Resolve>().GetType()); + Assert.Equal(typeof(ReplyHandler), endpointDependencyContainer.Resolve>().GetType()); + } + + // IErrorHandler + { + var expectedErrorHandlers = new[] + { + typeof(RetryErrorHandler), + typeof(TracingErrorHandler) + }; + + var actualErrorHandlers = endpointDependencyContainer + .ResolveCollection() + .Select(obj => obj.GetType()) + .ShowTypes(nameof(IErrorHandler), log) + .ToList(); + + Assert.Equal(expectedErrorHandlers, actualErrorHandlers); + } + } + } + } + + [Theory(Timeout = 60_000)] + [MemberData(nameof(HostBuilderTestData))] + internal void HostBuilder_throws_exception_if_there_are_endpoint_duplicates( + DirectoryInfo settingsDirectory, + TransportIdentity transportIdentity, + Func useTransport) + { + var endpointIdentity = TestIdentity.Endpoint10; + + InvalidOperationException? exception; + + try + { + _ = Fixture + .CreateHostBuilder() + .UseIntegrationTransport(transportIdentity, useTransport) + .UseEndpoint(endpointIdentity, builder => builder.BuildOptions()) + .UseEndpoint(endpointIdentity, builder => builder.BuildOptions()) + .BuildHost(settingsDirectory); + + exception = null; + } + catch (InvalidOperationException ex) + { + exception = ex; + } + + Assert.NotNull(exception); + Assert.Equal($"Endpoint duplicates was found: {endpointIdentity.LogicalName}. Horizontal scaling in the same process doesn't make sense.", exception.Message); + } + + [Theory(Timeout = 60_000)] + [MemberData(nameof(HostBuilderTestData))] + internal void HostBuilder_throws_exception_if_UseWebApiGateway_method_was_called_before_endpoint_declaration( + DirectoryInfo settingsDirectory, + TransportIdentity transportIdentity, + Func useTransport) + { + InvalidOperationException? exception; + + try + { + _ = Fixture + .CreateHostBuilder() + .UseIntegrationTransport(transportIdentity, useTransport) + .UseWebApiGateway() + .UseEndpoint(TestIdentity.Endpoint10, builder => builder.BuildOptions()) + .BuildHost(settingsDirectory); + + exception = null; + } + catch (InvalidOperationException ex) + { + exception = ex; + } + + Assert.NotNull(exception); + Assert.Equal(".UseWebApiGateway() should be called after all endpoint declarations", exception.Message); + } + + [Theory(Timeout = 60_000)] + [MemberData(nameof(HostBuilderTestData))] + internal void HostBuilder_throws_exception_if_UseIntegrationTransport_method_was_called_after_endpoint_declaration( + DirectoryInfo settingsDirectory, + TransportIdentity transportIdentity, + Func useTransport) + { + InvalidOperationException? exception; + + try + { + _ = Fixture + .CreateHostBuilder() + .UseEndpoint(TestIdentity.Endpoint10, builder => builder.BuildOptions()) + .UseIntegrationTransport(transportIdentity, useTransport) + .BuildHost(settingsDirectory); + + exception = null; + } + catch (InvalidOperationException ex) + { + exception = ex; + } + + Assert.NotNull(exception); + Assert.Equal(".UseIntegrationTransport() should be called before any endpoint declarations", exception.Message); + } + + [Theory(Timeout = 60_000)] + [MemberData(nameof(HostBuilderTestData))] + internal async Task Host_starts_and_stops_gracefully( + DirectoryInfo settingsDirectory, + TransportIdentity transportIdentity, + Func useTransport) + { + var host = Fixture + .CreateHostBuilder() + .UseIntegrationTransport(transportIdentity, useTransport) + .UseEndpoint(TestIdentity.Endpoint10, builder => builder.BuildOptions()) + .BuildHost(settingsDirectory); + + await host + .RunTestHost(Output, TestCase, StartStopTestInternal(settingsDirectory, transportIdentity)) + .ConfigureAwait(false); + + static Func StartStopTestInternal( + DirectoryInfo settingsDirectory, + TransportIdentity transportIdentity) + { + return (_, host, _) => + { + var transportDependencyContainer = host.GetIntegrationTransportDependencyContainer(transportIdentity); + + if (transportDependencyContainer.Resolve() is RabbitMqIntegrationTransport) + { + var rabbitMqSettings = transportDependencyContainer + .Resolve>() + .Get(); + + Assert.Equal(settingsDirectory.Name, rabbitMqSettings.VirtualHost); + } + + return Task.CompletedTask; + }; + } + } + } +} \ No newline at end of file diff --git a/tests/Tests/GenericHost.Test/LinqToSqlTests.cs b/tests/Tests/GenericHost.Test/LinqToSqlTests.cs new file mode 100644 index 00000000..41ab6a75 --- /dev/null +++ b/tests/Tests/GenericHost.Test/LinqToSqlTests.cs @@ -0,0 +1,1595 @@ +namespace SpaceEngineers.Core.GenericHost.Test +{ + using System; + using System.Collections.Generic; + using System.Data; + using System.Diagnostics.CodeAnalysis; + using System.Linq; + using System.Linq.Expressions; + using System.Reflection; + using System.Text.Json.Nodes; + using System.Threading; + using System.Threading.Tasks; + using AuthEndpoint.Domain; + using Basics; + using Basics.Primitives; + using CompositionRoot; + using CompositionRoot.Registration; + using Core.Test.Api.ClassFixtures; + using CrossCuttingConcerns.Settings; + using DataAccess.Orm.Sql.Linq; + using DataAccess.Orm.Sql.Migrations.Model; + using DataAccess.Orm.Sql.Model; + using DataAccess.Orm.Sql.Settings; + using DataAccess.Orm.Sql.Transaction; + using DataAccess.Orm.Sql.Translation; + using DatabaseEntities; + using DatabaseEntities.Relations; + using GenericDomain.EventSourcing.Sql; + using GenericEndpoint.DataAccess.Sql.Postgres.Host; + using GenericEndpoint.Host; + using IntegrationTransport.Host; + using Messages; + using Microsoft.Extensions.Hosting; + using Mocks; + using Registrations; + using SpaceEngineers.Core.Test.Api; + using StartupActions; + using Xunit; + using Xunit.Abstractions; + + /// + /// LinqToSql tests + /// + [SuppressMessage("Analysis", "CA1505", Justification = "sql test cases")] + [SuppressMessage("Analysis", "CA1506", Justification = "application composition root")] + [SuppressMessage("Analysis", "SA1131", Justification = "test case")] + public class LinqToSqlTests : TestBase + { + private static TestFixture? _staticFixture; + + /// .ctor + /// ITestOutputHelper + /// ModulesTestFixture + public LinqToSqlTests(ITestOutputHelper output, TestFixture fixture) + : base(output, fixture) + { + _staticFixture = fixture; + } + + private static TestFixture StaticFixture => _staticFixture ?? throw new InvalidOperationException(nameof(_staticFixture)); + + /// + /// Test cases for LinqToSql tests + /// + /// Test data + public static IEnumerable LinqToSqlTestData() + { + var hosts = CommandTranslationTestHosts().ToArray(); + var testCases = CommandTranslationTestCases().ToArray(); + var countdownEvent = new AsyncCountdownEvent(testCases.Length); + + return hosts + .SelectMany(host => testCases + .Select(testCase => host + .Concat(new object[] { countdownEvent }) + .Concat(testCase) + .ToArray())); + } + + internal static IEnumerable CommandTranslationTestHosts() + { + var projectFileDirectory = SolutionExtensions.ProjectFile().Directory + ?? throw new InvalidOperationException("Project directory wasn't found"); + + var settingsDirectory = projectFileDirectory + .StepInto("Settings") + .StepInto("LinqToSqlTest"); + + var timeout = TimeSpan.FromSeconds(60); + + var cts = new CancellationTokenSource(timeout); + + var host = new Lazy(() => + { + var hostBuilder = StaticFixture.CreateHostBuilder(); + + var databaseEntities = new[] + { + typeof(DatabaseDomainEvent), + typeof(DatabaseEntity), + typeof(ComplexDatabaseEntity), + typeof(Blog), + typeof(Post), + typeof(DatabaseEntities.Relations.User), + typeof(Community), + typeof(Participant) + }; + + var startupActions = new[] + { + typeof(CreateOrGetExistedPostgreSqlDatabaseHostedServiceStartupAction) + }; + + var additionalOurTypes = databaseEntities + .Concat(startupActions) + .ToArray(); + + var manualRegistrations = new IManualRegistration[] + { + new QueryExpressionsCollectorManualRegistration() + }; + + var host = hostBuilder + .UseInMemoryIntegrationTransport(IntegrationTransport.InMemory.Identity.TransportIdentity()) + .UseEndpoint(TestIdentity.Endpoint10, + builder => builder + .WithPostgreSqlDataAccess(options => options + .ExecuteMigrations()) + .ModifyContainerOptions(options => options + .WithAdditionalOurTypes(additionalOurTypes) + .WithManualRegistrations(manualRegistrations)) + .BuildOptions()) + .BuildHost(settingsDirectory); + + host + .StartAsync(cts.Token) + .Wait(cts.Token); + + return host; + }, + LazyThreadSafetyMode.ExecutionAndPublication); + + yield return new object[] { host, cts }; + } + + internal static IEnumerable CommandTranslationTestCases() + { + var schema = nameof(GenericHost) + nameof(Test); + + var databaseEntity = DatabaseEntity.Generate(); + + var aggregateId = Guid.NewGuid(); + var username = "SpaceEngineer"; + var password = "12345678"; + var salt = Password.GenerateSalt(); + var passwordHash = new Password(password).GeneratePasswordHash(salt); + var domainEvent = new DatabaseDomainEvent( + Guid.NewGuid(), + aggregateId, + 0, + DateTime.UtcNow, + new UserWasCreated(aggregateId, new Username(username), salt, passwordHash)); + + var user = new DatabaseEntities.Relations.User(Guid.NewGuid(), "SpaceEngineer"); + var posts = new List(); + var blog = new Blog(Guid.NewGuid(), "MilkyWay", posts); + var post = new Post(Guid.NewGuid(), blog, user, DateTime.Now, "PostContent"); + posts.Add(post); + + var communities = new List(); + var participants = new List(); + var community = new Community(Guid.NewGuid(), "AmazingCommunity", participants); + var participant = new Participant(Guid.NewGuid(), "RegularParticipant", communities); + communities.Add(community); + participants.Add(participant); + + var message = new Command(42); + + var complexDatabaseEntity = ComplexDatabaseEntity.Generate(message, blog); + var complexDatabaseEntityWithNulls = ComplexDatabaseEntity.GenerateWithNulls(message, blog); + + var token = CancellationToken.None; + + /* + * All + */ + yield return new object[] + { + $"{nameof(DataAccess.Orm.Sql.Postgres)} - all", + new Func(container => container.Resolve().All()), + new Action( + (query, log) => CheckSqlCommand(query, + $@"SELECT{Environment.NewLine}{'\t'}a.""{nameof(DatabaseEntity.BooleanField)}"",{Environment.NewLine}{'\t'}a.""{nameof(DatabaseEntity.Enum)}"",{Environment.NewLine}{'\t'}a.""{nameof(DatabaseEntity.IntField)}"",{Environment.NewLine}{'\t'}a.""{nameof(DatabaseEntity.NullableStringField)}"",{Environment.NewLine}{'\t'}a.""{nameof(DatabaseEntity.PrimaryKey)}"",{Environment.NewLine}{'\t'}a.""{nameof(DatabaseEntity.StringField)}"",{Environment.NewLine}{'\t'}a.""{nameof(DatabaseEntity.Version)}""{Environment.NewLine}FROM{Environment.NewLine}{'\t'}""{schema}"".""{nameof(DatabaseEntity)}"" a", + Array.Empty(), + log)), + new IDatabaseEntity[] { databaseEntity } + }; + yield return new object[] + { + $"{nameof(DataAccess.Orm.Sql.Postgres)} - read\\write complex object (with relations, arrays, json, nullable columns) without null values", + new Func(container => container.Resolve().All()), + new Action( + (query, log) => CheckSqlCommand(query, + $@"SELECT{Environment.NewLine}{'\t'}a.""{nameof(ComplexDatabaseEntity.Boolean)}"",{Environment.NewLine}{'\t'}a.""{nameof(ComplexDatabaseEntity.ByteArray)}"",{Environment.NewLine}{'\t'}a.""{nameof(ComplexDatabaseEntity.DateOnly)}"",{Environment.NewLine}{'\t'}a.""{nameof(ComplexDatabaseEntity.DateTime)}"",{Environment.NewLine}{'\t'}a.""{nameof(ComplexDatabaseEntity.DateTimeArray)}"",{Environment.NewLine}{'\t'}a.""{nameof(ComplexDatabaseEntity.Enum)}"",{Environment.NewLine}{'\t'}a.""{nameof(ComplexDatabaseEntity.EnumArray)}"",{Environment.NewLine}{'\t'}a.""{nameof(ComplexDatabaseEntity.EnumFlags)}"",{Environment.NewLine}{'\t'}a.""{nameof(ComplexDatabaseEntity.Identifier)}"",{Environment.NewLine}{'\t'}a.""{nameof(ComplexDatabaseEntity.Json)}"",{Environment.NewLine}{'\t'}a.""{nameof(ComplexDatabaseEntity.NullableBoolean)}"",{Environment.NewLine}{'\t'}a.""{nameof(ComplexDatabaseEntity.NullableDateOnly)}"",{Environment.NewLine}{'\t'}a.""{nameof(ComplexDatabaseEntity.NullableDateTime)}"",{Environment.NewLine}{'\t'}a.""{nameof(ComplexDatabaseEntity.NullableDateTimeArray)}"",{Environment.NewLine}{'\t'}a.""{nameof(ComplexDatabaseEntity.NullableEnum)}"",{Environment.NewLine}{'\t'}a.""{nameof(ComplexDatabaseEntity.NullableEnumArray)}"",{Environment.NewLine}{'\t'}a.""{nameof(ComplexDatabaseEntity.NullableEnumFlags)}"",{Environment.NewLine}{'\t'}a.""{nameof(ComplexDatabaseEntity.NullableIdentifier)}"",{Environment.NewLine}{'\t'}a.""{nameof(ComplexDatabaseEntity.NullableJson)}"",{Environment.NewLine}{'\t'}a.""{nameof(ComplexDatabaseEntity.NullableNumber)}"",{Environment.NewLine}{'\t'}a.""{nameof(ComplexDatabaseEntity.NullableRelation)}"",{Environment.NewLine}{'\t'}a.""{nameof(ComplexDatabaseEntity.NullableString)}"",{Environment.NewLine}{'\t'}a.""{nameof(ComplexDatabaseEntity.NullableStringArray)}"",{Environment.NewLine}{'\t'}a.""{nameof(ComplexDatabaseEntity.NullableTimeOnly)}"",{Environment.NewLine}{'\t'}a.""{nameof(ComplexDatabaseEntity.NullableTimeSpan)}"",{Environment.NewLine}{'\t'}a.""{nameof(ComplexDatabaseEntity.Number)}"",{Environment.NewLine}{'\t'}a.""{nameof(ComplexDatabaseEntity.PrimaryKey)}"",{Environment.NewLine}{'\t'}a.""{nameof(ComplexDatabaseEntity.Relation)}"",{Environment.NewLine}{'\t'}a.""{nameof(ComplexDatabaseEntity.String)}"",{Environment.NewLine}{'\t'}a.""{nameof(ComplexDatabaseEntity.StringArray)}"",{Environment.NewLine}{'\t'}a.""{nameof(ComplexDatabaseEntity.TimeOnly)}"",{Environment.NewLine}{'\t'}a.""{nameof(ComplexDatabaseEntity.TimeSpan)}"",{Environment.NewLine}{'\t'}a.""{nameof(ComplexDatabaseEntity.Version)}""{Environment.NewLine}FROM{Environment.NewLine}{'\t'}""{schema}"".""{nameof(ComplexDatabaseEntity)}"" a", + Array.Empty(), + log)), + new IDatabaseEntity[] { complexDatabaseEntity } + }; + yield return new object[] + { + $"{nameof(DataAccess.Orm.Sql.Postgres)} - read\\write complex object (with relations, arrays, json, nullable columns) with null values", + new Func(container => container.Resolve().All()), + new Action( + (query, log) => CheckSqlCommand(query, + $@"SELECT{Environment.NewLine}{'\t'}a.""{nameof(ComplexDatabaseEntity.Boolean)}"",{Environment.NewLine}{'\t'}a.""{nameof(ComplexDatabaseEntity.ByteArray)}"",{Environment.NewLine}{'\t'}a.""{nameof(ComplexDatabaseEntity.DateOnly)}"",{Environment.NewLine}{'\t'}a.""{nameof(ComplexDatabaseEntity.DateTime)}"",{Environment.NewLine}{'\t'}a.""{nameof(ComplexDatabaseEntity.DateTimeArray)}"",{Environment.NewLine}{'\t'}a.""{nameof(ComplexDatabaseEntity.Enum)}"",{Environment.NewLine}{'\t'}a.""{nameof(ComplexDatabaseEntity.EnumArray)}"",{Environment.NewLine}{'\t'}a.""{nameof(ComplexDatabaseEntity.EnumFlags)}"",{Environment.NewLine}{'\t'}a.""{nameof(ComplexDatabaseEntity.Identifier)}"",{Environment.NewLine}{'\t'}a.""{nameof(ComplexDatabaseEntity.Json)}"",{Environment.NewLine}{'\t'}a.""{nameof(ComplexDatabaseEntity.NullableBoolean)}"",{Environment.NewLine}{'\t'}a.""{nameof(ComplexDatabaseEntity.NullableDateOnly)}"",{Environment.NewLine}{'\t'}a.""{nameof(ComplexDatabaseEntity.NullableDateTime)}"",{Environment.NewLine}{'\t'}a.""{nameof(ComplexDatabaseEntity.NullableDateTimeArray)}"",{Environment.NewLine}{'\t'}a.""{nameof(ComplexDatabaseEntity.NullableEnum)}"",{Environment.NewLine}{'\t'}a.""{nameof(ComplexDatabaseEntity.NullableEnumArray)}"",{Environment.NewLine}{'\t'}a.""{nameof(ComplexDatabaseEntity.NullableEnumFlags)}"",{Environment.NewLine}{'\t'}a.""{nameof(ComplexDatabaseEntity.NullableIdentifier)}"",{Environment.NewLine}{'\t'}a.""{nameof(ComplexDatabaseEntity.NullableJson)}"",{Environment.NewLine}{'\t'}a.""{nameof(ComplexDatabaseEntity.NullableNumber)}"",{Environment.NewLine}{'\t'}a.""{nameof(ComplexDatabaseEntity.NullableRelation)}"",{Environment.NewLine}{'\t'}a.""{nameof(ComplexDatabaseEntity.NullableString)}"",{Environment.NewLine}{'\t'}a.""{nameof(ComplexDatabaseEntity.NullableStringArray)}"",{Environment.NewLine}{'\t'}a.""{nameof(ComplexDatabaseEntity.NullableTimeOnly)}"",{Environment.NewLine}{'\t'}a.""{nameof(ComplexDatabaseEntity.NullableTimeSpan)}"",{Environment.NewLine}{'\t'}a.""{nameof(ComplexDatabaseEntity.Number)}"",{Environment.NewLine}{'\t'}a.""{nameof(ComplexDatabaseEntity.PrimaryKey)}"",{Environment.NewLine}{'\t'}a.""{nameof(ComplexDatabaseEntity.Relation)}"",{Environment.NewLine}{'\t'}a.""{nameof(ComplexDatabaseEntity.String)}"",{Environment.NewLine}{'\t'}a.""{nameof(ComplexDatabaseEntity.StringArray)}"",{Environment.NewLine}{'\t'}a.""{nameof(ComplexDatabaseEntity.TimeOnly)}"",{Environment.NewLine}{'\t'}a.""{nameof(ComplexDatabaseEntity.TimeSpan)}"",{Environment.NewLine}{'\t'}a.""{nameof(ComplexDatabaseEntity.Version)}""{Environment.NewLine}FROM{Environment.NewLine}{'\t'}""{schema}"".""{nameof(ComplexDatabaseEntity)}"" a", + Array.Empty(), + log)), + new IDatabaseEntity[] { complexDatabaseEntityWithNulls } + }; + yield return new object[] + { + $"{nameof(DataAccess.Orm.Sql.Postgres)} - all mtm", + new Func(container => container.Resolve().AllMtm(container.Resolve(), it => it.Participants)), + new Action( + (query, log) => CheckSqlCommand(query, + $@"SELECT{Environment.NewLine}{'\t'}a.""{nameof(BaseMtmDatabaseEntity.Left)}"",{Environment.NewLine}{'\t'}a.""{nameof(BaseMtmDatabaseEntity.Right)}""{Environment.NewLine}FROM{Environment.NewLine}{'\t'}""{schema}"".""{nameof(Community)}_{nameof(Participant)}"" a", + Array.Empty(), + log)), + new IDatabaseEntity[] { community, participant } + }; + yield return new object[] + { + $"{nameof(DataAccess.Orm.Sql.Postgres)} - all mtm (reverse)", + new Func(container => container.Resolve().AllMtm(container.Resolve(), it => it.Communities)), + new Action( + (query, log) => CheckSqlCommand(query, + $@"SELECT{Environment.NewLine}{'\t'}a.""{nameof(BaseMtmDatabaseEntity.Left)}"",{Environment.NewLine}{'\t'}a.""{nameof(BaseMtmDatabaseEntity.Right)}""{Environment.NewLine}FROM{Environment.NewLine}{'\t'}""{schema}"".""{nameof(Community)}_{nameof(Participant)}"" a", + Array.Empty(), + log)), + new IDatabaseEntity[] { community, participant } + }; + yield return new object[] + { + $"{nameof(DataAccess.Orm.Sql.Postgres)} - all with circular mtm", + new Func(container => container.Resolve().All()), + new Action( + (query, log) => CheckSqlCommand(query, + $@"SELECT{Environment.NewLine}{'\t'}a.""{nameof(Community.Name)}"",{Environment.NewLine}{'\t'}a.""{nameof(Community.PrimaryKey)}"",{Environment.NewLine}{'\t'}a.""{nameof(Community.Version)}""{Environment.NewLine}FROM{Environment.NewLine}{'\t'}""{schema}"".""{nameof(Community)}"" a", + Array.Empty(), + log)), + new IDatabaseEntity[] { community, participant } + }; + yield return new object[] + { + $"{nameof(DataAccess.Orm.Sql.Postgres)} - all with circular mtm (reverse)", + new Func(container => container.Resolve().All()), + new Action( + (query, log) => CheckSqlCommand(query, + $@"SELECT{Environment.NewLine}{'\t'}a.""{nameof(Participant.Name)}"",{Environment.NewLine}{'\t'}a.""{nameof(Participant.PrimaryKey)}"",{Environment.NewLine}{'\t'}a.""{nameof(Participant.Version)}""{Environment.NewLine}FROM{Environment.NewLine}{'\t'}""{schema}"".""{nameof(Participant)}"" a", + Array.Empty(), + log)), + new IDatabaseEntity[] { community, participant } + }; + + /* + * Unary expression + */ + + yield return new object[] + { + $"{nameof(DataAccess.Orm.Sql.Postgres)} - unary filter", + new Func(container => container.Resolve().All().Where(it => !it.BooleanField || it.BooleanField)), + new Action( + (query, log) => CheckSqlCommand(query, + $@"SELECT{Environment.NewLine}{'\t'}a.""{nameof(DatabaseEntity.BooleanField)}"",{Environment.NewLine}{'\t'}a.""{nameof(DatabaseEntity.Enum)}"",{Environment.NewLine}{'\t'}a.""{nameof(DatabaseEntity.IntField)}"",{Environment.NewLine}{'\t'}a.""{nameof(DatabaseEntity.NullableStringField)}"",{Environment.NewLine}{'\t'}a.""{nameof(DatabaseEntity.PrimaryKey)}"",{Environment.NewLine}{'\t'}a.""{nameof(DatabaseEntity.StringField)}"",{Environment.NewLine}{'\t'}a.""{nameof(DatabaseEntity.Version)}""{Environment.NewLine}FROM{Environment.NewLine}{'\t'}""{schema}"".""{nameof(DatabaseEntity)}"" a{Environment.NewLine}WHERE{Environment.NewLine}{'\t'}NOT a.""{nameof(DatabaseEntity.BooleanField)}"" OR a.""{nameof(DatabaseEntity.BooleanField)}""", + Array.Empty(), + log)), + new IDatabaseEntity[] { databaseEntity } + }; + yield return new object[] + { + $"{nameof(DataAccess.Orm.Sql.Postgres)} - unary projection to anonymous class", + new Func(container => container.Resolve().All().Select(it => new { Negation = !it.BooleanField })), + new Action( + (query, log) => CheckSqlCommand(query, + $@"SELECT{Environment.NewLine}{'\t'}(NOT a.""{nameof(DatabaseEntity.BooleanField)}"") AS ""Negation""{Environment.NewLine}FROM{Environment.NewLine}{'\t'}""{schema}"".""{nameof(DatabaseEntity)}"" a", + Array.Empty(), + log)), + new IDatabaseEntity[] { databaseEntity } + }; + yield return new object[] + { + $"{nameof(DataAccess.Orm.Sql.Postgres)} - unary projection", + new Func(container => container.Resolve().All().Select(it => !it.BooleanField)), + new Action( + (query, log) => CheckSqlCommand(query, + $@"SELECT{Environment.NewLine}{'\t'}NOT a.""{nameof(DatabaseEntity.BooleanField)}""{Environment.NewLine}FROM{Environment.NewLine}{'\t'}""{schema}"".""{nameof(DatabaseEntity)}"" a", + Array.Empty(), + log)), + new IDatabaseEntity[] { databaseEntity } + }; + + /* + * Binary expression + */ + + yield return new object[] + { + $"{nameof(DataAccess.Orm.Sql.Postgres)} - binary comparison !=", + new Func(container => container.Resolve().All().Where(it => it.IntField != 43)), + new Action( + (query, log) => CheckSqlCommand(query, + $@"SELECT{Environment.NewLine}{'\t'}a.""{nameof(DatabaseEntity.BooleanField)}"",{Environment.NewLine}{'\t'}a.""{nameof(DatabaseEntity.Enum)}"",{Environment.NewLine}{'\t'}a.""{nameof(DatabaseEntity.IntField)}"",{Environment.NewLine}{'\t'}a.""{nameof(DatabaseEntity.NullableStringField)}"",{Environment.NewLine}{'\t'}a.""{nameof(DatabaseEntity.PrimaryKey)}"",{Environment.NewLine}{'\t'}a.""{nameof(DatabaseEntity.StringField)}"",{Environment.NewLine}{'\t'}a.""{nameof(DatabaseEntity.Version)}""{Environment.NewLine}FROM{Environment.NewLine}{'\t'}""{schema}"".""{nameof(DatabaseEntity)}"" a{Environment.NewLine}WHERE{Environment.NewLine}{'\t'}a.""{nameof(DatabaseEntity.IntField)}"" != @param_0", + new[] { new SqlCommandParameter("param_0", 43, typeof(int)) }, + log)), + new IDatabaseEntity[] { databaseEntity } + }; + yield return new object[] + { + $"{nameof(DataAccess.Orm.Sql.Postgres)} - binary comparison <", + new Func(container => container.Resolve().All().Where(it => it.IntField < 43)), + new Action( + (query, log) => CheckSqlCommand(query, + $@"SELECT{Environment.NewLine}{'\t'}a.""{nameof(DatabaseEntity.BooleanField)}"",{Environment.NewLine}{'\t'}a.""{nameof(DatabaseEntity.Enum)}"",{Environment.NewLine}{'\t'}a.""{nameof(DatabaseEntity.IntField)}"",{Environment.NewLine}{'\t'}a.""{nameof(DatabaseEntity.NullableStringField)}"",{Environment.NewLine}{'\t'}a.""{nameof(DatabaseEntity.PrimaryKey)}"",{Environment.NewLine}{'\t'}a.""{nameof(DatabaseEntity.StringField)}"",{Environment.NewLine}{'\t'}a.""{nameof(DatabaseEntity.Version)}""{Environment.NewLine}FROM{Environment.NewLine}{'\t'}""{schema}"".""{nameof(DatabaseEntity)}"" a{Environment.NewLine}WHERE{Environment.NewLine}{'\t'}a.""{nameof(DatabaseEntity.IntField)}"" < @param_0", + new[] { new SqlCommandParameter("param_0", 43, typeof(int)) }, + log)), + new IDatabaseEntity[] { databaseEntity } + }; + yield return new object[] + { + $"{nameof(DataAccess.Orm.Sql.Postgres)} - binary comparison <=", + new Func(container => container.Resolve().All().Where(it => it.IntField <= 42)), + new Action( + (query, log) => CheckSqlCommand(query, + $@"SELECT{Environment.NewLine}{'\t'}a.""{nameof(DatabaseEntity.BooleanField)}"",{Environment.NewLine}{'\t'}a.""{nameof(DatabaseEntity.Enum)}"",{Environment.NewLine}{'\t'}a.""{nameof(DatabaseEntity.IntField)}"",{Environment.NewLine}{'\t'}a.""{nameof(DatabaseEntity.NullableStringField)}"",{Environment.NewLine}{'\t'}a.""{nameof(DatabaseEntity.PrimaryKey)}"",{Environment.NewLine}{'\t'}a.""{nameof(DatabaseEntity.StringField)}"",{Environment.NewLine}{'\t'}a.""{nameof(DatabaseEntity.Version)}""{Environment.NewLine}FROM{Environment.NewLine}{'\t'}""{schema}"".""{nameof(DatabaseEntity)}"" a{Environment.NewLine}WHERE{Environment.NewLine}{'\t'}a.""{nameof(DatabaseEntity.IntField)}"" <= @param_0", + new[] { new SqlCommandParameter("param_0", 42, typeof(int)) }, + log)), + new IDatabaseEntity[] { databaseEntity } + }; + yield return new object[] + { + $"{nameof(DataAccess.Orm.Sql.Postgres)} - binary comparison ==", + new Func(container => container.Resolve().All().Where(it => it.IntField == 42)), + new Action( + (query, log) => CheckSqlCommand(query, + $@"SELECT{Environment.NewLine}{'\t'}a.""{nameof(DatabaseEntity.BooleanField)}"",{Environment.NewLine}{'\t'}a.""{nameof(DatabaseEntity.Enum)}"",{Environment.NewLine}{'\t'}a.""{nameof(DatabaseEntity.IntField)}"",{Environment.NewLine}{'\t'}a.""{nameof(DatabaseEntity.NullableStringField)}"",{Environment.NewLine}{'\t'}a.""{nameof(DatabaseEntity.PrimaryKey)}"",{Environment.NewLine}{'\t'}a.""{nameof(DatabaseEntity.StringField)}"",{Environment.NewLine}{'\t'}a.""{nameof(DatabaseEntity.Version)}""{Environment.NewLine}FROM{Environment.NewLine}{'\t'}""{schema}"".""{nameof(DatabaseEntity)}"" a{Environment.NewLine}WHERE{Environment.NewLine}{'\t'}a.""{nameof(DatabaseEntity.IntField)}"" = @param_0", + new[] { new SqlCommandParameter("param_0", 42, typeof(int)) }, + log)), + new IDatabaseEntity[] { databaseEntity } + }; + yield return new object[] + { + $"{nameof(DataAccess.Orm.Sql.Postgres)} - binary comparison >", + new Func(container => container.Resolve().All().Where(it => it.IntField > 41)), + new Action( + (query, log) => CheckSqlCommand(query, + $@"SELECT{Environment.NewLine}{'\t'}a.""{nameof(DatabaseEntity.BooleanField)}"",{Environment.NewLine}{'\t'}a.""{nameof(DatabaseEntity.Enum)}"",{Environment.NewLine}{'\t'}a.""{nameof(DatabaseEntity.IntField)}"",{Environment.NewLine}{'\t'}a.""{nameof(DatabaseEntity.NullableStringField)}"",{Environment.NewLine}{'\t'}a.""{nameof(DatabaseEntity.PrimaryKey)}"",{Environment.NewLine}{'\t'}a.""{nameof(DatabaseEntity.StringField)}"",{Environment.NewLine}{'\t'}a.""{nameof(DatabaseEntity.Version)}""{Environment.NewLine}FROM{Environment.NewLine}{'\t'}""{schema}"".""{nameof(DatabaseEntity)}"" a{Environment.NewLine}WHERE{Environment.NewLine}{'\t'}a.""{nameof(DatabaseEntity.IntField)}"" > @param_0", + new[] { new SqlCommandParameter("param_0", 41, typeof(int)) }, + log)), + new IDatabaseEntity[] { databaseEntity } + }; + yield return new object[] + { + $"{nameof(DataAccess.Orm.Sql.Postgres)} - binary comparison >=", + new Func(container => container.Resolve().All().Where(it => it.IntField >= 42)), + new Action( + (query, log) => CheckSqlCommand(query, + $@"SELECT{Environment.NewLine}{'\t'}a.""{nameof(DatabaseEntity.BooleanField)}"",{Environment.NewLine}{'\t'}a.""{nameof(DatabaseEntity.Enum)}"",{Environment.NewLine}{'\t'}a.""{nameof(DatabaseEntity.IntField)}"",{Environment.NewLine}{'\t'}a.""{nameof(DatabaseEntity.NullableStringField)}"",{Environment.NewLine}{'\t'}a.""{nameof(DatabaseEntity.PrimaryKey)}"",{Environment.NewLine}{'\t'}a.""{nameof(DatabaseEntity.StringField)}"",{Environment.NewLine}{'\t'}a.""{nameof(DatabaseEntity.Version)}""{Environment.NewLine}FROM{Environment.NewLine}{'\t'}""{schema}"".""{nameof(DatabaseEntity)}"" a{Environment.NewLine}WHERE{Environment.NewLine}{'\t'}a.""{nameof(DatabaseEntity.IntField)}"" >= @param_0", + new[] { new SqlCommandParameter("param_0", 42, typeof(int)) }, + log)), + new IDatabaseEntity[] { databaseEntity } + }; + yield return new object[] + { + $"{nameof(DataAccess.Orm.Sql.Postgres)} - binary filter", + new Func(container => container.Resolve().All().Select(it => it.NullableStringField).Where(it => it != null)), + new Action( + (query, log) => CheckSqlCommand(query, + $@"SELECT{Environment.NewLine}{'\t'}a.""{nameof(DatabaseEntity.NullableStringField)}""{Environment.NewLine}FROM{Environment.NewLine}{'\t'}""{schema}"".""{nameof(DatabaseEntity)}"" a{Environment.NewLine}WHERE{Environment.NewLine}{'\t'}CASE WHEN @param_0 IS NULL THEN a.""{nameof(DatabaseEntity.NullableStringField)}"" IS NOT NULL ELSE a.""{nameof(DatabaseEntity.NullableStringField)}"" != @param_1 END", + new[] { new SqlCommandParameter("param_0", default(string), typeof(string)), new SqlCommandParameter("param_1", default(string), typeof(string)) }, + log)), + new IDatabaseEntity[] { databaseEntity } + }; + yield return new object[] + { + $"{nameof(DataAccess.Orm.Sql.Postgres)} - reverse binary filter", + new Func(container => container.Resolve().All().Select(it => it.NullableStringField).Where(it => null != it)), + new Action( + (query, log) => CheckSqlCommand(query, + $@"SELECT{Environment.NewLine}{'\t'}a.""{nameof(DatabaseEntity.NullableStringField)}""{Environment.NewLine}FROM{Environment.NewLine}{'\t'}""{schema}"".""{nameof(DatabaseEntity)}"" a{Environment.NewLine}WHERE{Environment.NewLine}{'\t'}CASE WHEN @param_0 IS NULL THEN a.""{nameof(DatabaseEntity.NullableStringField)}"" IS NOT NULL ELSE a.""{nameof(DatabaseEntity.NullableStringField)}"" != @param_1 END", + new[] { new SqlCommandParameter("param_0", default(string), typeof(string)), new SqlCommandParameter("param_1", default(string), typeof(string)) }, + log)), + new IDatabaseEntity[] { databaseEntity } + }; + + /* + * Ternary expression + */ + + yield return new object[] + { + $"{nameof(DataAccess.Orm.Sql.Postgres)} - ternary filter after projection with renaming", + new Func(container => container.Resolve().All().Select(it => new { it.StringField, Filter = it.NullableStringField }).Where(it => it.Filter != null ? true : false)), + new Action( + (query, log) => CheckSqlCommand(query, + $@"SELECT{Environment.NewLine}{'\t'}a.""{nameof(DatabaseEntity.StringField)}"",{Environment.NewLine}{'\t'}a.""{nameof(DatabaseEntity.NullableStringField)}"" AS ""Filter""{Environment.NewLine}FROM{Environment.NewLine}{'\t'}""{schema}"".""{nameof(DatabaseEntity)}"" a{Environment.NewLine}WHERE{Environment.NewLine}{'\t'}CASE WHEN CASE WHEN @param_0 IS NULL THEN a.""{nameof(DatabaseEntity.NullableStringField)}"" IS NOT NULL ELSE a.""{nameof(DatabaseEntity.NullableStringField)}"" != @param_1 END THEN @param_2 ELSE @param_3 END", + new[] { new SqlCommandParameter("param_0", default(string), typeof(string)), new SqlCommandParameter("param_1", default(string), typeof(string)), new SqlCommandParameter("param_2", true, typeof(bool)), new SqlCommandParameter("param_3", false, typeof(bool)) }, + log)), + new IDatabaseEntity[] { databaseEntity } + }; + yield return new object[] + { + $"{nameof(DataAccess.Orm.Sql.Postgres)} - ternary filter after projection", + new Func(container => container.Resolve().All().Select(it => new { it.StringField, it.NullableStringField }).Where(it => it.NullableStringField != null ? true : false)), + new Action( + (query, log) => CheckSqlCommand(query, + $@"SELECT{Environment.NewLine}{'\t'}a.""{nameof(DatabaseEntity.StringField)}"",{Environment.NewLine}{'\t'}a.""{nameof(DatabaseEntity.NullableStringField)}""{Environment.NewLine}FROM{Environment.NewLine}{'\t'}""{schema}"".""{nameof(DatabaseEntity)}"" a{Environment.NewLine}WHERE{Environment.NewLine}{'\t'}CASE WHEN CASE WHEN @param_0 IS NULL THEN a.""{nameof(DatabaseEntity.NullableStringField)}"" IS NOT NULL ELSE a.""{nameof(DatabaseEntity.NullableStringField)}"" != @param_1 END THEN @param_2 ELSE @param_3 END", + new[] { new SqlCommandParameter("param_0", default(string), typeof(string)), new SqlCommandParameter("param_1", default(string), typeof(string)), new SqlCommandParameter("param_2", true, typeof(bool)), new SqlCommandParameter("param_3", false, typeof(bool)) }, + log)), + new IDatabaseEntity[] { databaseEntity } + }; + yield return new object[] + { + $"{nameof(DataAccess.Orm.Sql.Postgres)} - ternary filter", + new Func(container => container.Resolve().All().Where(it => it.NullableStringField != null ? true : false)), + new Action( + (query, log) => CheckSqlCommand(query, + $@"SELECT{Environment.NewLine}{'\t'}a.""{nameof(DatabaseEntity.BooleanField)}"",{Environment.NewLine}{'\t'}a.""{nameof(DatabaseEntity.Enum)}"",{Environment.NewLine}{'\t'}a.""{nameof(DatabaseEntity.IntField)}"",{Environment.NewLine}{'\t'}a.""{nameof(DatabaseEntity.NullableStringField)}"",{Environment.NewLine}{'\t'}a.""{nameof(DatabaseEntity.PrimaryKey)}"",{Environment.NewLine}{'\t'}a.""{nameof(DatabaseEntity.StringField)}"",{Environment.NewLine}{'\t'}a.""{nameof(DatabaseEntity.Version)}""{Environment.NewLine}FROM{Environment.NewLine}{'\t'}""{schema}"".""{nameof(DatabaseEntity)}"" a{Environment.NewLine}WHERE{Environment.NewLine}{'\t'}CASE WHEN CASE WHEN @param_0 IS NULL THEN a.""{nameof(DatabaseEntity.NullableStringField)}"" IS NOT NULL ELSE a.""{nameof(DatabaseEntity.NullableStringField)}"" != @param_1 END THEN @param_2 ELSE @param_3 END", + new[] { new SqlCommandParameter("param_0", default(string), typeof(string)), new SqlCommandParameter("param_1", default(string), typeof(string)), new SqlCommandParameter("param_2", true, typeof(bool)), new SqlCommandParameter("param_3", false, typeof(bool)) }, + log)), + new IDatabaseEntity[] { databaseEntity } + }; + yield return new object[] + { + $"{nameof(DataAccess.Orm.Sql.Postgres)} - ternary projection", + new Func(container => container.Resolve().All().Select(it => it.NullableStringField != null ? it.NullableStringField : string.Empty)), + new Action( + (query, log) => CheckSqlCommand(query, + $@"SELECT{Environment.NewLine}{'\t'}CASE WHEN CASE WHEN @param_0 IS NULL THEN a.""{nameof(DatabaseEntity.NullableStringField)}"" IS NOT NULL ELSE a.""{nameof(DatabaseEntity.NullableStringField)}"" != @param_1 END THEN a.""{nameof(DatabaseEntity.NullableStringField)}"" ELSE @param_2 END{Environment.NewLine}FROM{Environment.NewLine}{'\t'}""{schema}"".""{nameof(DatabaseEntity)}"" a", + new[] { new SqlCommandParameter("param_0", default(string), typeof(string)), new SqlCommandParameter("param_1", default(string), typeof(string)), new SqlCommandParameter("param_2", string.Empty, typeof(string)) }, + log)), + new IDatabaseEntity[] { databaseEntity } + }; + + /* + * Filter expression + */ + + yield return new object[] + { + $"{nameof(DataAccess.Orm.Sql.Postgres)} - boolean property filter after anonymous projection", + new Func(container => container.Resolve().All().Select(it => new { it.BooleanField, it.StringField }).Where(it => it.BooleanField)), + new Action( + (query, log) => CheckSqlCommand(query, + $@"SELECT{Environment.NewLine}{'\t'}a.""{nameof(DatabaseEntity.BooleanField)}"",{Environment.NewLine}{'\t'}a.""{nameof(DatabaseEntity.StringField)}""{Environment.NewLine}FROM{Environment.NewLine}{'\t'}""{schema}"".""{nameof(DatabaseEntity)}"" a{Environment.NewLine}WHERE{Environment.NewLine}{'\t'}a.""{nameof(DatabaseEntity.BooleanField)}""", + Array.Empty(), + log)), + new IDatabaseEntity[] { databaseEntity } + }; + yield return new object[] + { + $"{nameof(DataAccess.Orm.Sql.Postgres)} - boolean property filter", + new Func(container => container.Resolve().All().Where(it => it.BooleanField)), + new Action( + (query, log) => CheckSqlCommand(query, + $@"SELECT{Environment.NewLine}{'\t'}a.""{nameof(DatabaseEntity.BooleanField)}"",{Environment.NewLine}{'\t'}a.""{nameof(DatabaseEntity.Enum)}"",{Environment.NewLine}{'\t'}a.""{nameof(DatabaseEntity.IntField)}"",{Environment.NewLine}{'\t'}a.""{nameof(DatabaseEntity.NullableStringField)}"",{Environment.NewLine}{'\t'}a.""{nameof(DatabaseEntity.PrimaryKey)}"",{Environment.NewLine}{'\t'}a.""{nameof(DatabaseEntity.StringField)}"",{Environment.NewLine}{'\t'}a.""{nameof(DatabaseEntity.Version)}""{Environment.NewLine}FROM{Environment.NewLine}{'\t'}""{schema}"".""{nameof(DatabaseEntity)}"" a{Environment.NewLine}WHERE{Environment.NewLine}{'\t'}a.""{nameof(DatabaseEntity.BooleanField)}""", + Array.Empty(), + log)), + new IDatabaseEntity[] { databaseEntity } + }; + yield return new object[] + { + $"{nameof(DataAccess.Orm.Sql.Postgres)} - projection/filter chain", + new Func(container => container.Resolve().All().Select(it => new { it.NullableStringField, it.StringField, it.IntField }).Select(it => new { it.NullableStringField, it.IntField }).Where(it => it.NullableStringField != null).Select(it => new { it.IntField }).Where(it => it.IntField > 0).Where(it => it.IntField <= 42).Select(it => it.IntField)), + new Action( + (query, log) => CheckSqlCommand(query, + $@"SELECT{Environment.NewLine}{'\t'}d.""{nameof(DatabaseEntity.IntField)}""{Environment.NewLine}FROM{Environment.NewLine}{'\t'}(SELECT{Environment.NewLine}{'\t'}{'\t'}c.""{nameof(DatabaseEntity.IntField)}""{Environment.NewLine}{'\t'}FROM{Environment.NewLine}{'\t'}{'\t'}(SELECT{Environment.NewLine}{'\t'}{'\t'}{'\t'}b.""{nameof(DatabaseEntity.NullableStringField)}"",{Environment.NewLine}{'\t'}{'\t'}{'\t'}b.""{nameof(DatabaseEntity.IntField)}""{Environment.NewLine}{'\t'}{'\t'}FROM{Environment.NewLine}{'\t'}{'\t'}{'\t'}(SELECT{Environment.NewLine}{'\t'}{'\t'}{'\t'}{'\t'}a.""{nameof(DatabaseEntity.NullableStringField)}"",{Environment.NewLine}{'\t'}{'\t'}{'\t'}{'\t'}a.""{nameof(DatabaseEntity.StringField)}"",{Environment.NewLine}{'\t'}{'\t'}{'\t'}{'\t'}a.""{nameof(DatabaseEntity.IntField)}""{Environment.NewLine}{'\t'}{'\t'}{'\t'}FROM{Environment.NewLine}{'\t'}{'\t'}{'\t'}{'\t'}""{schema}"".""{nameof(DatabaseEntity)}"" a) b{Environment.NewLine}{'\t'}{'\t'}WHERE{Environment.NewLine}{'\t'}{'\t'}{'\t'}CASE WHEN @param_0 IS NULL THEN b.""{nameof(DatabaseEntity.NullableStringField)}"" IS NOT NULL ELSE b.""{nameof(DatabaseEntity.NullableStringField)}"" != @param_1 END) c{Environment.NewLine}{'\t'}WHERE{Environment.NewLine}{'\t'}{'\t'}c.""{nameof(DatabaseEntity.IntField)}"" > @param_2 AND c.""{nameof(DatabaseEntity.IntField)}"" <= @param_3) d", + new[] { new SqlCommandParameter("param_0", default(string), typeof(string)), new SqlCommandParameter("param_1", default(string), typeof(string)), new SqlCommandParameter("param_2", 0, typeof(int)), new SqlCommandParameter("param_3", 42, typeof(int)) }, + log)), + new IDatabaseEntity[] { databaseEntity } + }; + + /* + * Projection expression + */ + + yield return new object[] + { + $"{nameof(DataAccess.Orm.Sql.Postgres)} - coalesce projection", + new Func(container => container.Resolve().All().Select(it => it.NullableStringField ?? string.Empty)), + new Action( + (query, log) => CheckSqlCommand(query, + $@"SELECT{Environment.NewLine}{'\t'}COALESCE(a.""{nameof(DatabaseEntity.NullableStringField)}"", @param_0){Environment.NewLine}FROM{Environment.NewLine}{'\t'}""{schema}"".""{nameof(DatabaseEntity)}"" a", + new[] { new SqlCommandParameter("param_0", string.Empty, typeof(string)) }, + log)), + new IDatabaseEntity[] { databaseEntity } + }; + yield return new object[] + { + $"{nameof(DataAccess.Orm.Sql.Postgres)} - distinct projection to anonymous type", + new Func(container => container.Resolve().All().Where(it => it.BooleanField).Select(it => new { it.StringField }).Distinct()), + new Action( + (query, log) => CheckSqlCommand(query, + $@"SELECT DISTINCT{Environment.NewLine}{'\t'}b.""{nameof(DatabaseEntity.StringField)}""{Environment.NewLine}FROM{Environment.NewLine}{'\t'}(SELECT{Environment.NewLine}{'\t'}{'\t'}a.""{nameof(DatabaseEntity.BooleanField)}"",{Environment.NewLine}{'\t'}{'\t'}a.""{nameof(DatabaseEntity.Enum)}"",{Environment.NewLine}{'\t'}{'\t'}a.""{nameof(DatabaseEntity.IntField)}"",{Environment.NewLine}{'\t'}{'\t'}a.""{nameof(DatabaseEntity.NullableStringField)}"",{Environment.NewLine}{'\t'}{'\t'}a.""{nameof(DatabaseEntity.PrimaryKey)}"",{Environment.NewLine}{'\t'}{'\t'}a.""{nameof(DatabaseEntity.StringField)}"",{Environment.NewLine}{'\t'}{'\t'}a.""{nameof(DatabaseEntity.Version)}""{Environment.NewLine}{'\t'}FROM{Environment.NewLine}{'\t'}{'\t'}""{schema}"".""{nameof(DatabaseEntity)}"" a{Environment.NewLine}{'\t'}WHERE{Environment.NewLine}{'\t'}{'\t'}a.""{nameof(DatabaseEntity.BooleanField)}"") b", + Array.Empty(), + log)), + new IDatabaseEntity[] { databaseEntity } + }; + yield return new object[] + { + $"{nameof(DataAccess.Orm.Sql.Postgres)} - distinct projection to primitive type", + new Func(container => container.Resolve().All().Where(it => it.BooleanField).Select(it => it.StringField).Distinct()), + new Action( + (query, log) => CheckSqlCommand(query, + $@"SELECT DISTINCT{Environment.NewLine}{'\t'}b.""{nameof(DatabaseEntity.StringField)}""{Environment.NewLine}FROM{Environment.NewLine}{'\t'}(SELECT{Environment.NewLine}{'\t'}{'\t'}a.""{nameof(DatabaseEntity.BooleanField)}"",{Environment.NewLine}{'\t'}{'\t'}a.""{nameof(DatabaseEntity.Enum)}"",{Environment.NewLine}{'\t'}{'\t'}a.""{nameof(DatabaseEntity.IntField)}"",{Environment.NewLine}{'\t'}{'\t'}a.""{nameof(DatabaseEntity.NullableStringField)}"",{Environment.NewLine}{'\t'}{'\t'}a.""{nameof(DatabaseEntity.PrimaryKey)}"",{Environment.NewLine}{'\t'}{'\t'}a.""{nameof(DatabaseEntity.StringField)}"",{Environment.NewLine}{'\t'}{'\t'}a.""{nameof(DatabaseEntity.Version)}""{Environment.NewLine}{'\t'}FROM{Environment.NewLine}{'\t'}{'\t'}""{schema}"".""{nameof(DatabaseEntity)}"" a{Environment.NewLine}{'\t'}WHERE{Environment.NewLine}{'\t'}{'\t'}a.""{nameof(DatabaseEntity.BooleanField)}"") b", + Array.Empty(), + log)), + new IDatabaseEntity[] { databaseEntity } + }; + yield return new object[] + { + $"{nameof(DataAccess.Orm.Sql.Postgres)} - distinct projection with join expression", + new Func(container => container.Resolve().All().Where(it => it.Blog.Theme == "MilkyWay").Select(it => it.User.Nickname).Distinct()), + new Action( + (query, log) => CheckSqlCommand(query, + $@"SELECT DISTINCT{Environment.NewLine}{'\t'}d.""{nameof(Post.User.Nickname)}""{Environment.NewLine}FROM{Environment.NewLine}{'\t'}""{schema}"".""{nameof(DatabaseEntities.Relations.User)}"" d{Environment.NewLine}JOIN{Environment.NewLine}{'\t'}(SELECT{Environment.NewLine}{'\t'}{'\t'}a.""{nameof(Post.Blog)}"",{Environment.NewLine}{'\t'}{'\t'}a.""{nameof(Post.DateTime)}"",{Environment.NewLine}{'\t'}{'\t'}a.""{nameof(Post.PrimaryKey)}"",{Environment.NewLine}{'\t'}{'\t'}a.""{nameof(Post.Text)}"",{Environment.NewLine}{'\t'}{'\t'}a.""{nameof(Post.User)}"",{Environment.NewLine}{'\t'}{'\t'}a.""{nameof(DatabaseEntity.Version)}""{Environment.NewLine}{'\t'}FROM{Environment.NewLine}{'\t'}{'\t'}""{schema}"".""{nameof(Blog)}"" b{Environment.NewLine}{'\t'}JOIN{Environment.NewLine}{'\t'}{'\t'}""{schema}"".""{nameof(Post)}"" a{Environment.NewLine}{'\t'}ON{Environment.NewLine}{'\t'}{'\t'}b.""{nameof(Blog.PrimaryKey)}"" = a.""{nameof(Post.Blog)}""{Environment.NewLine}{'\t'}WHERE{Environment.NewLine}{'\t'}{'\t'}CASE WHEN @param_0 IS NULL THEN b.""{nameof(Blog.Theme)}"" IS NULL ELSE b.""{nameof(Blog.Theme)}"" = @param_1 END) c{Environment.NewLine}ON{Environment.NewLine}{'\t'}d.""{nameof(DatabaseEntities.Relations.User.PrimaryKey)}"" = c.""{nameof(Post.User)}""", + new[] { new SqlCommandParameter("param_0", "MilkyWay", typeof(string)), new SqlCommandParameter("param_1", "MilkyWay", typeof(string)) }, + log)), + new IDatabaseEntity[] { user, blog, post } + }; + yield return new object[] + { + $"{nameof(DataAccess.Orm.Sql.Postgres)} - distinct projection with predicate", + new Func(container => container.Resolve().All().Select(it => new { it.StringField, it.BooleanField }).Where(it => it.BooleanField).Distinct()), + new Action( + (query, log) => CheckSqlCommand(query, + $@"SELECT DISTINCT{Environment.NewLine}{'\t'}a.""{nameof(DatabaseEntity.StringField)}"",{Environment.NewLine}{'\t'}a.""{nameof(DatabaseEntity.BooleanField)}""{Environment.NewLine}FROM{Environment.NewLine}{'\t'}""{schema}"".""{nameof(DatabaseEntity)}"" a{Environment.NewLine}WHERE{Environment.NewLine}{'\t'}a.""{nameof(DatabaseEntity.BooleanField)}""", + Array.Empty(), + log)), + new IDatabaseEntity[] { databaseEntity } + }; + + /* + * Anonymous projections + */ + + yield return new object[] + { + $"{nameof(DataAccess.Orm.Sql.Postgres)} - anonymous projections chain", + new Func(container => container.Resolve().All().Select(it => new { it.StringField, it.IntField, it.BooleanField }).Select(it => new { it.StringField, it.IntField }).Select(it => new { it.IntField }).Select(it => it.IntField)), + new Action( + (query, log) => CheckSqlCommand(query, + $@"SELECT{Environment.NewLine}{'\t'}d.""{nameof(DatabaseEntity.IntField)}""{Environment.NewLine}FROM{Environment.NewLine}{'\t'}(SELECT{Environment.NewLine}{'\t'}{'\t'}c.""{nameof(DatabaseEntity.IntField)}""{Environment.NewLine}{'\t'}FROM{Environment.NewLine}{'\t'}{'\t'}(SELECT{Environment.NewLine}{'\t'}{'\t'}{'\t'}b.""{nameof(DatabaseEntity.StringField)}"",{Environment.NewLine}{'\t'}{'\t'}{'\t'}b.""{nameof(DatabaseEntity.IntField)}""{Environment.NewLine}{'\t'}{'\t'}FROM{Environment.NewLine}{'\t'}{'\t'}{'\t'}(SELECT{Environment.NewLine}{'\t'}{'\t'}{'\t'}{'\t'}a.""{nameof(DatabaseEntity.StringField)}"",{Environment.NewLine}{'\t'}{'\t'}{'\t'}{'\t'}a.""{nameof(DatabaseEntity.IntField)}"",{Environment.NewLine}{'\t'}{'\t'}{'\t'}{'\t'}a.""{nameof(DatabaseEntity.BooleanField)}""{Environment.NewLine}{'\t'}{'\t'}{'\t'}FROM{Environment.NewLine}{'\t'}{'\t'}{'\t'}{'\t'}""{schema}"".""{nameof(DatabaseEntity)}"" a) b) c) d", + Array.Empty(), + log)), + new IDatabaseEntity[] { databaseEntity } + }; + yield return new object[] + { + $"{nameof(DataAccess.Orm.Sql.Postgres)} - change anonymous projection parameter name", + new Func(container => container.Resolve().All().Select(it => new { Nsf = it.NullableStringField, Sf = it.StringField }).Where(it => it.Nsf != null)), + new Action( + (query, log) => CheckSqlCommand(query, + $@"SELECT{Environment.NewLine}{'\t'}a.""{nameof(DatabaseEntity.NullableStringField)}"" AS ""Nsf"",{Environment.NewLine}{'\t'}a.""{nameof(DatabaseEntity.StringField)}"" AS ""Sf""{Environment.NewLine}FROM{Environment.NewLine}{'\t'}""{schema}"".""{nameof(DatabaseEntity)}"" a{Environment.NewLine}WHERE{Environment.NewLine}{'\t'}CASE WHEN @param_0 IS NULL THEN a.""{nameof(DatabaseEntity.NullableStringField)}"" IS NOT NULL ELSE a.""{nameof(DatabaseEntity.NullableStringField)}"" != @param_1 END", + new[] { new SqlCommandParameter("param_0", default(string), typeof(string)), new SqlCommandParameter("param_1", default(string), typeof(string)) }, + log)), + new IDatabaseEntity[] { databaseEntity } + }; + + /* + * Enums + */ + + yield return new object[] + { + $"{nameof(DataAccess.Orm.Sql.Postgres)} - enum property comparison", + new Func(container => container.Resolve().All().Where(it => it.Enum > EnEnum.Two)), + new Action( + (query, log) => CheckSqlCommand(query, + $@"SELECT{Environment.NewLine}{'\t'}a.""{nameof(DatabaseEntity.BooleanField)}"",{Environment.NewLine}{'\t'}a.""{nameof(DatabaseEntity.Enum)}"",{Environment.NewLine}{'\t'}a.""{nameof(DatabaseEntity.IntField)}"",{Environment.NewLine}{'\t'}a.""{nameof(DatabaseEntity.NullableStringField)}"",{Environment.NewLine}{'\t'}a.""{nameof(DatabaseEntity.PrimaryKey)}"",{Environment.NewLine}{'\t'}a.""{nameof(DatabaseEntity.StringField)}"",{Environment.NewLine}{'\t'}a.""{nameof(DatabaseEntity.Version)}""{Environment.NewLine}FROM{Environment.NewLine}{'\t'}""{schema}"".""{nameof(DatabaseEntity)}"" a{Environment.NewLine}WHERE{Environment.NewLine}{'\t'}a.""{nameof(DatabaseEntity.Enum)}"" > @param_0", + new[] { new SqlCommandParameter("param_0", EnEnum.Two, typeof(EnEnum)) }, + log)), + new IDatabaseEntity[] { databaseEntity } + }; + yield return new object[] + { + $"{nameof(DataAccess.Orm.Sql.Postgres)} - enum property filter (equals operator)", + new Func(container => container.Resolve().All().Where(it => it.Enum == EnEnum.Three)), + new Action( + (query, log) => CheckSqlCommand(query, + $@"SELECT{Environment.NewLine}{'\t'}a.""{nameof(DatabaseEntity.BooleanField)}"",{Environment.NewLine}{'\t'}a.""{nameof(DatabaseEntity.Enum)}"",{Environment.NewLine}{'\t'}a.""{nameof(DatabaseEntity.IntField)}"",{Environment.NewLine}{'\t'}a.""{nameof(DatabaseEntity.NullableStringField)}"",{Environment.NewLine}{'\t'}a.""{nameof(DatabaseEntity.PrimaryKey)}"",{Environment.NewLine}{'\t'}a.""{nameof(DatabaseEntity.StringField)}"",{Environment.NewLine}{'\t'}a.""{nameof(DatabaseEntity.Version)}""{Environment.NewLine}FROM{Environment.NewLine}{'\t'}""{schema}"".""{nameof(DatabaseEntity)}"" a{Environment.NewLine}WHERE{Environment.NewLine}{'\t'}a.""{nameof(DatabaseEntity.Enum)}"" = @param_0", + new[] { new SqlCommandParameter("param_0", EnEnum.Three, typeof(EnEnum)) }, + log)), + new IDatabaseEntity[] { databaseEntity } + }; + yield return new object[] + { + $"{nameof(DataAccess.Orm.Sql.Postgres)} - enum property filter (object.Equals)", + new Func(container => container.Resolve().All().Where(it => it.Enum.Equals(EnEnum.Three))), + new Action( + (query, log) => CheckSqlCommand(query, + $@"SELECT{Environment.NewLine}{'\t'}a.""{nameof(DatabaseEntity.BooleanField)}"",{Environment.NewLine}{'\t'}a.""{nameof(DatabaseEntity.Enum)}"",{Environment.NewLine}{'\t'}a.""{nameof(DatabaseEntity.IntField)}"",{Environment.NewLine}{'\t'}a.""{nameof(DatabaseEntity.NullableStringField)}"",{Environment.NewLine}{'\t'}a.""{nameof(DatabaseEntity.PrimaryKey)}"",{Environment.NewLine}{'\t'}a.""{nameof(DatabaseEntity.StringField)}"",{Environment.NewLine}{'\t'}a.""{nameof(DatabaseEntity.Version)}""{Environment.NewLine}FROM{Environment.NewLine}{'\t'}""{schema}"".""{nameof(DatabaseEntity)}"" a{Environment.NewLine}WHERE{Environment.NewLine}{'\t'}a.""{nameof(DatabaseEntity.Enum)}"" = @param_0", + new[] { new SqlCommandParameter("param_0", EnEnum.Three, typeof(EnEnum)) }, + log)), + new IDatabaseEntity[] { databaseEntity } + }; + yield return new object[] + { + $"{nameof(DataAccess.Orm.Sql.Postgres)} - enum property filter (array.Contains)", + new Func(container => container.Resolve().All().Where(it => new[] { EnEnum.Three }.Contains(it.Enum))), + new Action( + (query, log) => CheckSqlCommand(query, + $@"SELECT{Environment.NewLine}{'\t'}a.""{nameof(DatabaseEntity.BooleanField)}"",{Environment.NewLine}{'\t'}a.""{nameof(DatabaseEntity.Enum)}"",{Environment.NewLine}{'\t'}a.""{nameof(DatabaseEntity.IntField)}"",{Environment.NewLine}{'\t'}a.""{nameof(DatabaseEntity.NullableStringField)}"",{Environment.NewLine}{'\t'}a.""{nameof(DatabaseEntity.PrimaryKey)}"",{Environment.NewLine}{'\t'}a.""{nameof(DatabaseEntity.StringField)}"",{Environment.NewLine}{'\t'}a.""{nameof(DatabaseEntity.Version)}""{Environment.NewLine}FROM{Environment.NewLine}{'\t'}""{schema}"".""{nameof(DatabaseEntity)}"" a{Environment.NewLine}WHERE{Environment.NewLine}{'\t'}a.""{nameof(DatabaseEntity.Enum)}"" = ANY(@param_0)", + new[] { new SqlCommandParameter("param_0", new[] { EnEnum.Three }, typeof(EnEnum[])) }, + log)), + new IDatabaseEntity[] { databaseEntity } + }; + yield return new object[] + { + $"{nameof(DataAccess.Orm.Sql.Postgres)} - enum property filter (list.Contains)", + new Func(container => container.Resolve().All().Where(it => new List { EnEnum.Three }.Contains(it.Enum))), + new Action( + (query, log) => CheckSqlCommand(query, + $@"SELECT{Environment.NewLine}{'\t'}a.""{nameof(DatabaseEntity.BooleanField)}"",{Environment.NewLine}{'\t'}a.""{nameof(DatabaseEntity.Enum)}"",{Environment.NewLine}{'\t'}a.""{nameof(DatabaseEntity.IntField)}"",{Environment.NewLine}{'\t'}a.""{nameof(DatabaseEntity.NullableStringField)}"",{Environment.NewLine}{'\t'}a.""{nameof(DatabaseEntity.PrimaryKey)}"",{Environment.NewLine}{'\t'}a.""{nameof(DatabaseEntity.StringField)}"",{Environment.NewLine}{'\t'}a.""{nameof(DatabaseEntity.Version)}""{Environment.NewLine}FROM{Environment.NewLine}{'\t'}""{schema}"".""{nameof(DatabaseEntity)}"" a{Environment.NewLine}WHERE{Environment.NewLine}{'\t'}a.""{nameof(DatabaseEntity.Enum)}"" = ANY(@param_0)", + new[] { new SqlCommandParameter("param_0", new List { EnEnum.Three }, typeof(List)) }, + log)), + new IDatabaseEntity[] { databaseEntity } + }; + yield return new object[] + { + $"{nameof(DataAccess.Orm.Sql.Postgres)} - enum.HasFlag()", + new Func(container => container.Resolve().All().Select(it => it.EnumFlags.HasFlag(EnEnumFlags.B | EnEnumFlags.C))), + new Action( + (query, log) => CheckSqlCommand(query, + $@"SELECT{Environment.NewLine}{'\t'}a.""{nameof(ComplexDatabaseEntity.EnumFlags)}"" && @param_0{Environment.NewLine}FROM{Environment.NewLine}{'\t'}""{schema}"".""{nameof(ComplexDatabaseEntity)}"" a", + new[] { new SqlCommandParameter("param_0", EnEnumFlags.B | EnEnumFlags.C, typeof(EnEnumFlags)) }, + log)), + new IDatabaseEntity[] { complexDatabaseEntity } + }; + + /* + * one property projection + */ + + yield return new object[] + { + $"{nameof(DataAccess.Orm.Sql.Postgres)} - one property projection - bool", + new Func(container => container.Resolve().All().Select(it => it.BooleanField)), + new Action( + (query, log) => CheckSqlCommand(query, + $@"SELECT{Environment.NewLine}{'\t'}a.""{nameof(DatabaseEntity.BooleanField)}""{Environment.NewLine}FROM{Environment.NewLine}{'\t'}""{schema}"".""{nameof(DatabaseEntity)}"" a", + Array.Empty(), + log)), + new IDatabaseEntity[] { databaseEntity } + }; + yield return new object[] + { + $"{nameof(DataAccess.Orm.Sql.Postgres)} - one property projection - guid", + new Func(container => container.Resolve().All().Select(it => it.PrimaryKey)), + new Action( + (query, log) => CheckSqlCommand(query, + $@"SELECT{Environment.NewLine}{'\t'}a.""{nameof(DatabaseEntity.PrimaryKey)}""{Environment.NewLine}FROM{Environment.NewLine}{'\t'}""{schema}"".""{nameof(DatabaseEntity)}"" a", + Array.Empty(), + log)), + new IDatabaseEntity[] { databaseEntity } + }; + yield return new object[] + { + $"{nameof(DataAccess.Orm.Sql.Postgres)} - one property projection - int", + new Func(container => container.Resolve().All().Select(it => it.IntField)), + new Action( + (query, log) => CheckSqlCommand(query, + $@"SELECT{Environment.NewLine}{'\t'}a.""{nameof(DatabaseEntity.IntField)}""{Environment.NewLine}FROM{Environment.NewLine}{'\t'}""{schema}"".""{nameof(DatabaseEntity)}"" a", + Array.Empty(), + log)), + new IDatabaseEntity[] { databaseEntity } + }; + yield return new object[] + { + $"{nameof(DataAccess.Orm.Sql.Postgres)} - one property projection - string", + new Func(container => container.Resolve().All().Select(it => it.StringField)), + new Action( + (query, log) => CheckSqlCommand(query, + $@"SELECT{Environment.NewLine}{'\t'}a.""{nameof(DatabaseEntity.StringField)}""{Environment.NewLine}FROM{Environment.NewLine}{'\t'}""{schema}"".""{nameof(DatabaseEntity)}"" a", + Array.Empty(), + log)), + new IDatabaseEntity[] { databaseEntity } + }; + yield return new object[] + { + $"{nameof(DataAccess.Orm.Sql.Postgres)} - property chain with translated member", + new Func(container => container.Resolve().All().Select(it => it.StringField.Length)), + new Action( + (query, log) => CheckSqlCommand(query, + $@"SELECT{Environment.NewLine}{'\t'}length(a.""{nameof(DatabaseEntity.StringField)}""){Environment.NewLine}FROM{Environment.NewLine}{'\t'}""{schema}"".""{nameof(DatabaseEntity)}"" a", + Array.Empty(), + log)), + new IDatabaseEntity[] { databaseEntity } + }; + + /* + * Scalar + */ + + yield return new object[] + { + $"{nameof(DataAccess.Orm.Sql.Postgres)} - scalar result - all", + new Func(container => container.Resolve().All().All(it => it.BooleanField)), + new Action( + (query, log) => CheckSqlCommand(query, + $@"SELECT{Environment.NewLine}{'\t'}(Count(CASE WHEN a.""{nameof(DatabaseEntity.BooleanField)}"" THEN @param_0 ELSE NULL END) = Count(*)) AS ""All""{Environment.NewLine}FROM{Environment.NewLine}{'\t'}""{schema}"".""{nameof(DatabaseEntity)}"" a", + new[] { new SqlCommandParameter("param_0", 1, typeof(int)) }, + log)), + new IDatabaseEntity[] { databaseEntity } + }; + yield return new object[] + { + $"{nameof(DataAccess.Orm.Sql.Postgres)} - scalar result - all async", + new Func(container => container.Resolve().All().CachedExpression(Guid.NewGuid().ToString()).AllAsync(it => it.BooleanField, token).Result), + new Action( + (query, log) => CheckSqlCommand(query, + $@"SELECT{Environment.NewLine}{'\t'}(Count(CASE WHEN a.""{nameof(DatabaseEntity.BooleanField)}"" THEN @param_0 ELSE NULL END) = Count(*)) AS ""All""{Environment.NewLine}FROM{Environment.NewLine}{'\t'}""{schema}"".""{nameof(DatabaseEntity)}"" a", + new[] { new SqlCommandParameter("param_0", 1, typeof(int)) }, + log)), + new IDatabaseEntity[] { databaseEntity } + }; + yield return new object[] + { + $"{nameof(DataAccess.Orm.Sql.Postgres)} - scalar result - any", + new Func(container => container.Resolve().All().Where(it => it.BooleanField).Any()), + new Action( + (query, log) => CheckSqlCommand(query, + $@"SELECT{Environment.NewLine}{'\t'}(Count(*) > @param_0) AS ""Any""{Environment.NewLine}FROM{Environment.NewLine}{'\t'}(SELECT{Environment.NewLine}{'\t'}{'\t'}a.""{nameof(DatabaseEntity.BooleanField)}"",{Environment.NewLine}{'\t'}{'\t'}a.""{nameof(DatabaseEntity.Enum)}"",{Environment.NewLine}{'\t'}{'\t'}a.""{nameof(DatabaseEntity.IntField)}"",{Environment.NewLine}{'\t'}{'\t'}a.""{nameof(DatabaseEntity.NullableStringField)}"",{Environment.NewLine}{'\t'}{'\t'}a.""{nameof(DatabaseEntity.PrimaryKey)}"",{Environment.NewLine}{'\t'}{'\t'}a.""{nameof(DatabaseEntity.StringField)}"",{Environment.NewLine}{'\t'}{'\t'}a.""{nameof(DatabaseEntity.Version)}""{Environment.NewLine}{'\t'}FROM{Environment.NewLine}{'\t'}{'\t'}""{schema}"".""{nameof(DatabaseEntity)}"" a{Environment.NewLine}{'\t'}WHERE{Environment.NewLine}{'\t'}{'\t'}a.""{nameof(DatabaseEntity.BooleanField)}"") b", + new[] { new SqlCommandParameter("param_0", 0, typeof(int)) }, + log)), + new IDatabaseEntity[] { databaseEntity } + }; + yield return new object[] + { + $"{nameof(DataAccess.Orm.Sql.Postgres)} - scalar result - any 2", + new Func(container => container.Resolve().All().Any(it => it.BooleanField)), + new Action( + (query, log) => CheckSqlCommand(query, + $@"SELECT{Environment.NewLine}{'\t'}(Count(*) > @param_0) AS ""Any""{Environment.NewLine}FROM{Environment.NewLine}{'\t'}(SELECT{Environment.NewLine}{'\t'}{'\t'}a.""{nameof(DatabaseEntity.BooleanField)}"",{Environment.NewLine}{'\t'}{'\t'}a.""{nameof(DatabaseEntity.Enum)}"",{Environment.NewLine}{'\t'}{'\t'}a.""{nameof(DatabaseEntity.IntField)}"",{Environment.NewLine}{'\t'}{'\t'}a.""{nameof(DatabaseEntity.NullableStringField)}"",{Environment.NewLine}{'\t'}{'\t'}a.""{nameof(DatabaseEntity.PrimaryKey)}"",{Environment.NewLine}{'\t'}{'\t'}a.""{nameof(DatabaseEntity.StringField)}"",{Environment.NewLine}{'\t'}{'\t'}a.""{nameof(DatabaseEntity.Version)}""{Environment.NewLine}{'\t'}FROM{Environment.NewLine}{'\t'}{'\t'}""{schema}"".""{nameof(DatabaseEntity)}"" a{Environment.NewLine}{'\t'}WHERE{Environment.NewLine}{'\t'}{'\t'}a.""{nameof(DatabaseEntity.BooleanField)}"") b", + new[] { new SqlCommandParameter("param_0", 0, typeof(int)) }, + log)), + new IDatabaseEntity[] { databaseEntity } + }; + yield return new object[] + { + $"{nameof(DataAccess.Orm.Sql.Postgres)} - scalar result - any async", + new Func(container => container.Resolve().All().Where(it => it.BooleanField).CachedExpression(Guid.NewGuid().ToString()).AnyAsync(token).Result), + new Action( + (query, log) => CheckSqlCommand(query, + $@"SELECT{Environment.NewLine}{'\t'}(Count(*) > @param_0) AS ""Any""{Environment.NewLine}FROM{Environment.NewLine}{'\t'}(SELECT{Environment.NewLine}{'\t'}{'\t'}a.""{nameof(DatabaseEntity.BooleanField)}"",{Environment.NewLine}{'\t'}{'\t'}a.""{nameof(DatabaseEntity.Enum)}"",{Environment.NewLine}{'\t'}{'\t'}a.""{nameof(DatabaseEntity.IntField)}"",{Environment.NewLine}{'\t'}{'\t'}a.""{nameof(DatabaseEntity.NullableStringField)}"",{Environment.NewLine}{'\t'}{'\t'}a.""{nameof(DatabaseEntity.PrimaryKey)}"",{Environment.NewLine}{'\t'}{'\t'}a.""{nameof(DatabaseEntity.StringField)}"",{Environment.NewLine}{'\t'}{'\t'}a.""{nameof(DatabaseEntity.Version)}""{Environment.NewLine}{'\t'}FROM{Environment.NewLine}{'\t'}{'\t'}""{schema}"".""{nameof(DatabaseEntity)}"" a{Environment.NewLine}{'\t'}WHERE{Environment.NewLine}{'\t'}{'\t'}a.""{nameof(DatabaseEntity.BooleanField)}"") b", + new[] { new SqlCommandParameter("param_0", 0, typeof(int)) }, + log)), + new IDatabaseEntity[] { databaseEntity } + }; + yield return new object[] + { + $"{nameof(DataAccess.Orm.Sql.Postgres)} - scalar result - any async 2", + new Func(container => container.Resolve().All().CachedExpression(Guid.NewGuid().ToString()).AnyAsync(it => it.BooleanField, token).Result), + new Action( + (query, log) => CheckSqlCommand(query, + $@"SELECT{Environment.NewLine}{'\t'}(Count(*) > @param_0) AS ""Any""{Environment.NewLine}FROM{Environment.NewLine}{'\t'}(SELECT{Environment.NewLine}{'\t'}{'\t'}a.""{nameof(DatabaseEntity.BooleanField)}"",{Environment.NewLine}{'\t'}{'\t'}a.""{nameof(DatabaseEntity.Enum)}"",{Environment.NewLine}{'\t'}{'\t'}a.""{nameof(DatabaseEntity.IntField)}"",{Environment.NewLine}{'\t'}{'\t'}a.""{nameof(DatabaseEntity.NullableStringField)}"",{Environment.NewLine}{'\t'}{'\t'}a.""{nameof(DatabaseEntity.PrimaryKey)}"",{Environment.NewLine}{'\t'}{'\t'}a.""{nameof(DatabaseEntity.StringField)}"",{Environment.NewLine}{'\t'}{'\t'}a.""{nameof(DatabaseEntity.Version)}""{Environment.NewLine}{'\t'}FROM{Environment.NewLine}{'\t'}{'\t'}""{schema}"".""{nameof(DatabaseEntity)}"" a{Environment.NewLine}{'\t'}WHERE{Environment.NewLine}{'\t'}{'\t'}a.""{nameof(DatabaseEntity.BooleanField)}"") b", + new[] { new SqlCommandParameter("param_0", 0, typeof(int)) }, + log)), + new IDatabaseEntity[] { databaseEntity } + }; + yield return new object[] + { + $"{nameof(DataAccess.Orm.Sql.Postgres)} - scalar result - count", + new Func(container => container.Resolve().All().Where(it => it.BooleanField).Count()), + new Action( + (query, log) => CheckSqlCommand(query, + $@"SELECT{Environment.NewLine}{'\t'}(Count(*)) AS ""Count""{Environment.NewLine}FROM{Environment.NewLine}{'\t'}(SELECT{Environment.NewLine}{'\t'}{'\t'}a.""{nameof(DatabaseEntity.BooleanField)}"",{Environment.NewLine}{'\t'}{'\t'}a.""{nameof(DatabaseEntity.Enum)}"",{Environment.NewLine}{'\t'}{'\t'}a.""{nameof(DatabaseEntity.IntField)}"",{Environment.NewLine}{'\t'}{'\t'}a.""{nameof(DatabaseEntity.NullableStringField)}"",{Environment.NewLine}{'\t'}{'\t'}a.""{nameof(DatabaseEntity.PrimaryKey)}"",{Environment.NewLine}{'\t'}{'\t'}a.""{nameof(DatabaseEntity.StringField)}"",{Environment.NewLine}{'\t'}{'\t'}a.""{nameof(DatabaseEntity.Version)}""{Environment.NewLine}{'\t'}FROM{Environment.NewLine}{'\t'}{'\t'}""{schema}"".""{nameof(DatabaseEntity)}"" a{Environment.NewLine}{'\t'}WHERE{Environment.NewLine}{'\t'}{'\t'}a.""{nameof(DatabaseEntity.BooleanField)}"") b", + Array.Empty(), + log)), + new IDatabaseEntity[] { databaseEntity } + }; + yield return new object[] + { + $"{nameof(DataAccess.Orm.Sql.Postgres)} - scalar result - count 2", + new Func(container => container.Resolve().All().Count(it => it.BooleanField)), + new Action( + (query, log) => CheckSqlCommand(query, + $@"SELECT{Environment.NewLine}{'\t'}(Count(*)) AS ""Count""{Environment.NewLine}FROM{Environment.NewLine}{'\t'}(SELECT{Environment.NewLine}{'\t'}{'\t'}a.""{nameof(DatabaseEntity.BooleanField)}"",{Environment.NewLine}{'\t'}{'\t'}a.""{nameof(DatabaseEntity.Enum)}"",{Environment.NewLine}{'\t'}{'\t'}a.""{nameof(DatabaseEntity.IntField)}"",{Environment.NewLine}{'\t'}{'\t'}a.""{nameof(DatabaseEntity.NullableStringField)}"",{Environment.NewLine}{'\t'}{'\t'}a.""{nameof(DatabaseEntity.PrimaryKey)}"",{Environment.NewLine}{'\t'}{'\t'}a.""{nameof(DatabaseEntity.StringField)}"",{Environment.NewLine}{'\t'}{'\t'}a.""{nameof(DatabaseEntity.Version)}""{Environment.NewLine}{'\t'}FROM{Environment.NewLine}{'\t'}{'\t'}""{schema}"".""{nameof(DatabaseEntity)}"" a{Environment.NewLine}{'\t'}WHERE{Environment.NewLine}{'\t'}{'\t'}a.""{nameof(DatabaseEntity.BooleanField)}"") b", + Array.Empty(), + log)), + new IDatabaseEntity[] { databaseEntity } + }; + yield return new object[] + { + $"{nameof(DataAccess.Orm.Sql.Postgres)} - scalar result - count async", + new Func(container => container.Resolve().All().Where(it => it.BooleanField).CachedExpression(Guid.NewGuid().ToString()).CountAsync(token).Result), + new Action( + (query, log) => CheckSqlCommand(query, + $@"SELECT{Environment.NewLine}{'\t'}(Count(*)) AS ""Count""{Environment.NewLine}FROM{Environment.NewLine}{'\t'}(SELECT{Environment.NewLine}{'\t'}{'\t'}a.""{nameof(DatabaseEntity.BooleanField)}"",{Environment.NewLine}{'\t'}{'\t'}a.""{nameof(DatabaseEntity.Enum)}"",{Environment.NewLine}{'\t'}{'\t'}a.""{nameof(DatabaseEntity.IntField)}"",{Environment.NewLine}{'\t'}{'\t'}a.""{nameof(DatabaseEntity.NullableStringField)}"",{Environment.NewLine}{'\t'}{'\t'}a.""{nameof(DatabaseEntity.PrimaryKey)}"",{Environment.NewLine}{'\t'}{'\t'}a.""{nameof(DatabaseEntity.StringField)}"",{Environment.NewLine}{'\t'}{'\t'}a.""{nameof(DatabaseEntity.Version)}""{Environment.NewLine}{'\t'}FROM{Environment.NewLine}{'\t'}{'\t'}""{schema}"".""{nameof(DatabaseEntity)}"" a{Environment.NewLine}{'\t'}WHERE{Environment.NewLine}{'\t'}{'\t'}a.""{nameof(DatabaseEntity.BooleanField)}"") b", + Array.Empty(), + log)), + new IDatabaseEntity[] { databaseEntity } + }; + yield return new object[] + { + $"{nameof(DataAccess.Orm.Sql.Postgres)} - scalar result - count async 2", + new Func(container => container.Resolve().All().CachedExpression(Guid.NewGuid().ToString()).CountAsync(it => it.BooleanField, token).Result), + new Action( + (query, log) => CheckSqlCommand(query, + $@"SELECT{Environment.NewLine}{'\t'}(Count(*)) AS ""Count""{Environment.NewLine}FROM{Environment.NewLine}{'\t'}(SELECT{Environment.NewLine}{'\t'}{'\t'}a.""{nameof(DatabaseEntity.BooleanField)}"",{Environment.NewLine}{'\t'}{'\t'}a.""{nameof(DatabaseEntity.Enum)}"",{Environment.NewLine}{'\t'}{'\t'}a.""{nameof(DatabaseEntity.IntField)}"",{Environment.NewLine}{'\t'}{'\t'}a.""{nameof(DatabaseEntity.NullableStringField)}"",{Environment.NewLine}{'\t'}{'\t'}a.""{nameof(DatabaseEntity.PrimaryKey)}"",{Environment.NewLine}{'\t'}{'\t'}a.""{nameof(DatabaseEntity.StringField)}"",{Environment.NewLine}{'\t'}{'\t'}a.""{nameof(DatabaseEntity.Version)}""{Environment.NewLine}{'\t'}FROM{Environment.NewLine}{'\t'}{'\t'}""{schema}"".""{nameof(DatabaseEntity)}"" a{Environment.NewLine}{'\t'}WHERE{Environment.NewLine}{'\t'}{'\t'}a.""{nameof(DatabaseEntity.BooleanField)}"") b", + Array.Empty(), + log)), + new IDatabaseEntity[] { databaseEntity } + }; + yield return new object[] + { + $"{nameof(DataAccess.Orm.Sql.Postgres)} - scalar result - first", + new Func(container => container.Resolve().All().First(it => it.BooleanField)), + new Action( + (query, log) => CheckSqlCommand(query, + $@"SELECT{Environment.NewLine}{'\t'}a.""{nameof(DatabaseEntity.BooleanField)}"",{Environment.NewLine}{'\t'}a.""{nameof(DatabaseEntity.Enum)}"",{Environment.NewLine}{'\t'}a.""{nameof(DatabaseEntity.IntField)}"",{Environment.NewLine}{'\t'}a.""{nameof(DatabaseEntity.NullableStringField)}"",{Environment.NewLine}{'\t'}a.""{nameof(DatabaseEntity.PrimaryKey)}"",{Environment.NewLine}{'\t'}a.""{nameof(DatabaseEntity.StringField)}"",{Environment.NewLine}{'\t'}a.""{nameof(DatabaseEntity.Version)}""{Environment.NewLine}FROM{Environment.NewLine}{'\t'}""{schema}"".""{nameof(DatabaseEntity)}"" a{Environment.NewLine}WHERE{Environment.NewLine}{'\t'}a.""{nameof(DatabaseEntity.BooleanField)}""{Environment.NewLine}FETCH FIRST 1 ROWS ONLY", + Array.Empty(), + log)), + new IDatabaseEntity[] { databaseEntity } + }; + yield return new object[] + { + $"{nameof(DataAccess.Orm.Sql.Postgres)} - scalar result - first 2", + new Func(container => container.Resolve().All().Where(it => it.BooleanField).First()), + new Action( + (query, log) => CheckSqlCommand(query, + $@"SELECT{Environment.NewLine}{'\t'}a.""{nameof(DatabaseEntity.BooleanField)}"",{Environment.NewLine}{'\t'}a.""{nameof(DatabaseEntity.Enum)}"",{Environment.NewLine}{'\t'}a.""{nameof(DatabaseEntity.IntField)}"",{Environment.NewLine}{'\t'}a.""{nameof(DatabaseEntity.NullableStringField)}"",{Environment.NewLine}{'\t'}a.""{nameof(DatabaseEntity.PrimaryKey)}"",{Environment.NewLine}{'\t'}a.""{nameof(DatabaseEntity.StringField)}"",{Environment.NewLine}{'\t'}a.""{nameof(DatabaseEntity.Version)}""{Environment.NewLine}FROM{Environment.NewLine}{'\t'}""{schema}"".""{nameof(DatabaseEntity)}"" a{Environment.NewLine}WHERE{Environment.NewLine}{'\t'}a.""{nameof(DatabaseEntity.BooleanField)}""{Environment.NewLine}FETCH FIRST 1 ROWS ONLY", + Array.Empty(), + log)), + new IDatabaseEntity[] { databaseEntity } + }; + yield return new object[] + { + $"{nameof(DataAccess.Orm.Sql.Postgres)} - scalar result - first async", + new Func(container => container.Resolve().All().CachedExpression(Guid.NewGuid().ToString()).FirstAsync(it => it.BooleanField, token).Result), + new Action( + (query, log) => CheckSqlCommand(query, + $@"SELECT{Environment.NewLine}{'\t'}a.""{nameof(DatabaseEntity.BooleanField)}"",{Environment.NewLine}{'\t'}a.""{nameof(DatabaseEntity.Enum)}"",{Environment.NewLine}{'\t'}a.""{nameof(DatabaseEntity.IntField)}"",{Environment.NewLine}{'\t'}a.""{nameof(DatabaseEntity.NullableStringField)}"",{Environment.NewLine}{'\t'}a.""{nameof(DatabaseEntity.PrimaryKey)}"",{Environment.NewLine}{'\t'}a.""{nameof(DatabaseEntity.StringField)}"",{Environment.NewLine}{'\t'}a.""{nameof(DatabaseEntity.Version)}""{Environment.NewLine}FROM{Environment.NewLine}{'\t'}""{schema}"".""{nameof(DatabaseEntity)}"" a{Environment.NewLine}WHERE{Environment.NewLine}{'\t'}a.""{nameof(DatabaseEntity.BooleanField)}""{Environment.NewLine}FETCH FIRST 1 ROWS ONLY", + Array.Empty(), + log)), + new IDatabaseEntity[] { databaseEntity } + }; + yield return new object[] + { + $"{nameof(DataAccess.Orm.Sql.Postgres)} - scalar result - first async 2", + new Func(container => container.Resolve().All().Where(it => it.BooleanField).CachedExpression(Guid.NewGuid().ToString()).FirstAsync(token).Result), + new Action( + (query, log) => CheckSqlCommand(query, + $@"SELECT{Environment.NewLine}{'\t'}a.""{nameof(DatabaseEntity.BooleanField)}"",{Environment.NewLine}{'\t'}a.""{nameof(DatabaseEntity.Enum)}"",{Environment.NewLine}{'\t'}a.""{nameof(DatabaseEntity.IntField)}"",{Environment.NewLine}{'\t'}a.""{nameof(DatabaseEntity.NullableStringField)}"",{Environment.NewLine}{'\t'}a.""{nameof(DatabaseEntity.PrimaryKey)}"",{Environment.NewLine}{'\t'}a.""{nameof(DatabaseEntity.StringField)}"",{Environment.NewLine}{'\t'}a.""{nameof(DatabaseEntity.Version)}""{Environment.NewLine}FROM{Environment.NewLine}{'\t'}""{schema}"".""{nameof(DatabaseEntity)}"" a{Environment.NewLine}WHERE{Environment.NewLine}{'\t'}a.""{nameof(DatabaseEntity.BooleanField)}""{Environment.NewLine}FETCH FIRST 1 ROWS ONLY", + Array.Empty(), + log)), + new IDatabaseEntity[] { databaseEntity } + }; + yield return new object[] + { + $"{nameof(DataAccess.Orm.Sql.Postgres)} - scalar result - first or default", + new Func(container => container.Resolve().All().FirstOrDefault(it => it.BooleanField)), + new Action( + (query, log) => CheckSqlCommand(query, + $@"SELECT{Environment.NewLine}{'\t'}a.""{nameof(DatabaseEntity.BooleanField)}"",{Environment.NewLine}{'\t'}a.""{nameof(DatabaseEntity.Enum)}"",{Environment.NewLine}{'\t'}a.""{nameof(DatabaseEntity.IntField)}"",{Environment.NewLine}{'\t'}a.""{nameof(DatabaseEntity.NullableStringField)}"",{Environment.NewLine}{'\t'}a.""{nameof(DatabaseEntity.PrimaryKey)}"",{Environment.NewLine}{'\t'}a.""{nameof(DatabaseEntity.StringField)}"",{Environment.NewLine}{'\t'}a.""{nameof(DatabaseEntity.Version)}""{Environment.NewLine}FROM{Environment.NewLine}{'\t'}""{schema}"".""{nameof(DatabaseEntity)}"" a{Environment.NewLine}WHERE{Environment.NewLine}{'\t'}a.""{nameof(DatabaseEntity.BooleanField)}""{Environment.NewLine}FETCH FIRST 1 ROWS ONLY", + Array.Empty(), + log)), + new IDatabaseEntity[] { databaseEntity } + }; + yield return new object[] + { + $"{nameof(DataAccess.Orm.Sql.Postgres)} - scalar result - first or default 2", + new Func(container => container.Resolve().All().Where(it => it.BooleanField).FirstOrDefault()), + new Action( + (query, log) => CheckSqlCommand(query, + $@"SELECT{Environment.NewLine}{'\t'}a.""{nameof(DatabaseEntity.BooleanField)}"",{Environment.NewLine}{'\t'}a.""{nameof(DatabaseEntity.Enum)}"",{Environment.NewLine}{'\t'}a.""{nameof(DatabaseEntity.IntField)}"",{Environment.NewLine}{'\t'}a.""{nameof(DatabaseEntity.NullableStringField)}"",{Environment.NewLine}{'\t'}a.""{nameof(DatabaseEntity.PrimaryKey)}"",{Environment.NewLine}{'\t'}a.""{nameof(DatabaseEntity.StringField)}"",{Environment.NewLine}{'\t'}a.""{nameof(DatabaseEntity.Version)}""{Environment.NewLine}FROM{Environment.NewLine}{'\t'}""{schema}"".""{nameof(DatabaseEntity)}"" a{Environment.NewLine}WHERE{Environment.NewLine}{'\t'}a.""{nameof(DatabaseEntity.BooleanField)}""{Environment.NewLine}FETCH FIRST 1 ROWS ONLY", + Array.Empty(), + log)), + new IDatabaseEntity[] { databaseEntity } + }; + yield return new object[] + { + $"{nameof(DataAccess.Orm.Sql.Postgres)} - scalar result - first or default async", + new Func(container => container.Resolve().All().CachedExpression(Guid.NewGuid().ToString()).FirstOrDefaultAsync(it => it.BooleanField, token).Result), + new Action( + (query, log) => CheckSqlCommand(query, + $@"SELECT{Environment.NewLine}{'\t'}a.""{nameof(DatabaseEntity.BooleanField)}"",{Environment.NewLine}{'\t'}a.""{nameof(DatabaseEntity.Enum)}"",{Environment.NewLine}{'\t'}a.""{nameof(DatabaseEntity.IntField)}"",{Environment.NewLine}{'\t'}a.""{nameof(DatabaseEntity.NullableStringField)}"",{Environment.NewLine}{'\t'}a.""{nameof(DatabaseEntity.PrimaryKey)}"",{Environment.NewLine}{'\t'}a.""{nameof(DatabaseEntity.StringField)}"",{Environment.NewLine}{'\t'}a.""{nameof(DatabaseEntity.Version)}""{Environment.NewLine}FROM{Environment.NewLine}{'\t'}""{schema}"".""{nameof(DatabaseEntity)}"" a{Environment.NewLine}WHERE{Environment.NewLine}{'\t'}a.""{nameof(DatabaseEntity.BooleanField)}""{Environment.NewLine}FETCH FIRST 1 ROWS ONLY", + Array.Empty(), + log)), + new IDatabaseEntity[] { databaseEntity } + }; + yield return new object[] + { + $"{nameof(DataAccess.Orm.Sql.Postgres)} - scalar result - first or default async 2", + new Func(container => container.Resolve().All().Where(it => it.BooleanField).CachedExpression(Guid.NewGuid().ToString()).FirstOrDefaultAsync(token).Result), + new Action( + (query, log) => CheckSqlCommand(query, + $@"SELECT{Environment.NewLine}{'\t'}a.""{nameof(DatabaseEntity.BooleanField)}"",{Environment.NewLine}{'\t'}a.""{nameof(DatabaseEntity.Enum)}"",{Environment.NewLine}{'\t'}a.""{nameof(DatabaseEntity.IntField)}"",{Environment.NewLine}{'\t'}a.""{nameof(DatabaseEntity.NullableStringField)}"",{Environment.NewLine}{'\t'}a.""{nameof(DatabaseEntity.PrimaryKey)}"",{Environment.NewLine}{'\t'}a.""{nameof(DatabaseEntity.StringField)}"",{Environment.NewLine}{'\t'}a.""{nameof(DatabaseEntity.Version)}""{Environment.NewLine}FROM{Environment.NewLine}{'\t'}""{schema}"".""{nameof(DatabaseEntity)}"" a{Environment.NewLine}WHERE{Environment.NewLine}{'\t'}a.""{nameof(DatabaseEntity.BooleanField)}""{Environment.NewLine}FETCH FIRST 1 ROWS ONLY", + Array.Empty(), + log)), + new IDatabaseEntity[] { databaseEntity } + }; + yield return new object[] + { + $"{nameof(DataAccess.Orm.Sql.Postgres)} - scalar result - single", + new Func(container => container.Resolve().All().Single(it => it.BooleanField)), + new Action( + (query, log) => CheckSqlCommand(query, + $@"SELECT{Environment.NewLine}{'\t'}a.""{nameof(DatabaseEntity.BooleanField)}"",{Environment.NewLine}{'\t'}a.""{nameof(DatabaseEntity.Enum)}"",{Environment.NewLine}{'\t'}a.""{nameof(DatabaseEntity.IntField)}"",{Environment.NewLine}{'\t'}a.""{nameof(DatabaseEntity.NullableStringField)}"",{Environment.NewLine}{'\t'}a.""{nameof(DatabaseEntity.PrimaryKey)}"",{Environment.NewLine}{'\t'}a.""{nameof(DatabaseEntity.StringField)}"",{Environment.NewLine}{'\t'}a.""{nameof(DatabaseEntity.Version)}""{Environment.NewLine}FROM{Environment.NewLine}{'\t'}""{schema}"".""{nameof(DatabaseEntity)}"" a{Environment.NewLine}WHERE{Environment.NewLine}{'\t'}a.""{nameof(DatabaseEntity.BooleanField)}""{Environment.NewLine}FETCH FIRST 2 ROWS ONLY", + Array.Empty(), + log)), + new IDatabaseEntity[] { databaseEntity } + }; + yield return new object[] + { + $"{nameof(DataAccess.Orm.Sql.Postgres)} - scalar result - single 2", + new Func(container => container.Resolve().All().Where(it => it.BooleanField).Single()), + new Action( + (query, log) => CheckSqlCommand(query, + $@"SELECT{Environment.NewLine}{'\t'}a.""{nameof(DatabaseEntity.BooleanField)}"",{Environment.NewLine}{'\t'}a.""{nameof(DatabaseEntity.Enum)}"",{Environment.NewLine}{'\t'}a.""{nameof(DatabaseEntity.IntField)}"",{Environment.NewLine}{'\t'}a.""{nameof(DatabaseEntity.NullableStringField)}"",{Environment.NewLine}{'\t'}a.""{nameof(DatabaseEntity.PrimaryKey)}"",{Environment.NewLine}{'\t'}a.""{nameof(DatabaseEntity.StringField)}"",{Environment.NewLine}{'\t'}a.""{nameof(DatabaseEntity.Version)}""{Environment.NewLine}FROM{Environment.NewLine}{'\t'}""{schema}"".""{nameof(DatabaseEntity)}"" a{Environment.NewLine}WHERE{Environment.NewLine}{'\t'}a.""{nameof(DatabaseEntity.BooleanField)}""{Environment.NewLine}FETCH FIRST 2 ROWS ONLY", + Array.Empty(), + log)), + new IDatabaseEntity[] { databaseEntity } + }; + yield return new object[] + { + $"{nameof(DataAccess.Orm.Sql.Postgres)} - scalar result - single async", + new Func(container => container.Resolve().All().CachedExpression(Guid.NewGuid().ToString()).SingleAsync(it => it.BooleanField, token).Result), + new Action( + (query, log) => CheckSqlCommand(query, + $@"SELECT{Environment.NewLine}{'\t'}a.""{nameof(DatabaseEntity.BooleanField)}"",{Environment.NewLine}{'\t'}a.""{nameof(DatabaseEntity.Enum)}"",{Environment.NewLine}{'\t'}a.""{nameof(DatabaseEntity.IntField)}"",{Environment.NewLine}{'\t'}a.""{nameof(DatabaseEntity.NullableStringField)}"",{Environment.NewLine}{'\t'}a.""{nameof(DatabaseEntity.PrimaryKey)}"",{Environment.NewLine}{'\t'}a.""{nameof(DatabaseEntity.StringField)}"",{Environment.NewLine}{'\t'}a.""{nameof(DatabaseEntity.Version)}""{Environment.NewLine}FROM{Environment.NewLine}{'\t'}""{schema}"".""{nameof(DatabaseEntity)}"" a{Environment.NewLine}WHERE{Environment.NewLine}{'\t'}a.""{nameof(DatabaseEntity.BooleanField)}""{Environment.NewLine}FETCH FIRST 2 ROWS ONLY", + Array.Empty(), + log)), + new IDatabaseEntity[] { databaseEntity } + }; + yield return new object[] + { + $"{nameof(DataAccess.Orm.Sql.Postgres)} - scalar result - single async 2", + new Func(container => container.Resolve().All().Where(it => it.BooleanField).CachedExpression(Guid.NewGuid().ToString()).SingleAsync(token).Result), + new Action( + (query, log) => CheckSqlCommand(query, + $@"SELECT{Environment.NewLine}{'\t'}a.""{nameof(DatabaseEntity.BooleanField)}"",{Environment.NewLine}{'\t'}a.""{nameof(DatabaseEntity.Enum)}"",{Environment.NewLine}{'\t'}a.""{nameof(DatabaseEntity.IntField)}"",{Environment.NewLine}{'\t'}a.""{nameof(DatabaseEntity.NullableStringField)}"",{Environment.NewLine}{'\t'}a.""{nameof(DatabaseEntity.PrimaryKey)}"",{Environment.NewLine}{'\t'}a.""{nameof(DatabaseEntity.StringField)}"",{Environment.NewLine}{'\t'}a.""{nameof(DatabaseEntity.Version)}""{Environment.NewLine}FROM{Environment.NewLine}{'\t'}""{schema}"".""{nameof(DatabaseEntity)}"" a{Environment.NewLine}WHERE{Environment.NewLine}{'\t'}a.""{nameof(DatabaseEntity.BooleanField)}""{Environment.NewLine}FETCH FIRST 2 ROWS ONLY", + Array.Empty(), + log)), + new IDatabaseEntity[] { databaseEntity } + }; + yield return new object[] + { + $"{nameof(DataAccess.Orm.Sql.Postgres)} - scalar result - single or default", + new Func(container => container.Resolve().All().SingleOrDefault(it => it.BooleanField)), + new Action( + (query, log) => CheckSqlCommand(query, + $@"SELECT{Environment.NewLine}{'\t'}a.""{nameof(DatabaseEntity.BooleanField)}"",{Environment.NewLine}{'\t'}a.""{nameof(DatabaseEntity.Enum)}"",{Environment.NewLine}{'\t'}a.""{nameof(DatabaseEntity.IntField)}"",{Environment.NewLine}{'\t'}a.""{nameof(DatabaseEntity.NullableStringField)}"",{Environment.NewLine}{'\t'}a.""{nameof(DatabaseEntity.PrimaryKey)}"",{Environment.NewLine}{'\t'}a.""{nameof(DatabaseEntity.StringField)}"",{Environment.NewLine}{'\t'}a.""{nameof(DatabaseEntity.Version)}""{Environment.NewLine}FROM{Environment.NewLine}{'\t'}""{schema}"".""{nameof(DatabaseEntity)}"" a{Environment.NewLine}WHERE{Environment.NewLine}{'\t'}a.""{nameof(DatabaseEntity.BooleanField)}""{Environment.NewLine}FETCH FIRST 2 ROWS ONLY", + Array.Empty(), + log)), + new IDatabaseEntity[] { databaseEntity } + }; + yield return new object[] + { + $"{nameof(DataAccess.Orm.Sql.Postgres)} - scalar result - single or default 2", + new Func(container => container.Resolve().All().Where(it => it.BooleanField).SingleOrDefault()), + new Action( + (query, log) => CheckSqlCommand(query, + $@"SELECT{Environment.NewLine}{'\t'}a.""{nameof(DatabaseEntity.BooleanField)}"",{Environment.NewLine}{'\t'}a.""{nameof(DatabaseEntity.Enum)}"",{Environment.NewLine}{'\t'}a.""{nameof(DatabaseEntity.IntField)}"",{Environment.NewLine}{'\t'}a.""{nameof(DatabaseEntity.NullableStringField)}"",{Environment.NewLine}{'\t'}a.""{nameof(DatabaseEntity.PrimaryKey)}"",{Environment.NewLine}{'\t'}a.""{nameof(DatabaseEntity.StringField)}"",{Environment.NewLine}{'\t'}a.""{nameof(DatabaseEntity.Version)}""{Environment.NewLine}FROM{Environment.NewLine}{'\t'}""{schema}"".""{nameof(DatabaseEntity)}"" a{Environment.NewLine}WHERE{Environment.NewLine}{'\t'}a.""{nameof(DatabaseEntity.BooleanField)}""{Environment.NewLine}FETCH FIRST 2 ROWS ONLY", + Array.Empty(), + log)), + new IDatabaseEntity[] { databaseEntity } + }; + yield return new object[] + { + $"{nameof(DataAccess.Orm.Sql.Postgres)} - scalar result - single or default async", + new Func(container => container.Resolve().All().CachedExpression(Guid.NewGuid().ToString()).SingleOrDefaultAsync(it => it.BooleanField, token).Result), + new Action( + (query, log) => CheckSqlCommand(query, + $@"SELECT{Environment.NewLine}{'\t'}a.""{nameof(DatabaseEntity.BooleanField)}"",{Environment.NewLine}{'\t'}a.""{nameof(DatabaseEntity.Enum)}"",{Environment.NewLine}{'\t'}a.""{nameof(DatabaseEntity.IntField)}"",{Environment.NewLine}{'\t'}a.""{nameof(DatabaseEntity.NullableStringField)}"",{Environment.NewLine}{'\t'}a.""{nameof(DatabaseEntity.PrimaryKey)}"",{Environment.NewLine}{'\t'}a.""{nameof(DatabaseEntity.StringField)}"",{Environment.NewLine}{'\t'}a.""{nameof(DatabaseEntity.Version)}""{Environment.NewLine}FROM{Environment.NewLine}{'\t'}""{schema}"".""{nameof(DatabaseEntity)}"" a{Environment.NewLine}WHERE{Environment.NewLine}{'\t'}a.""{nameof(DatabaseEntity.BooleanField)}""{Environment.NewLine}FETCH FIRST 2 ROWS ONLY", + Array.Empty(), + log)), + new IDatabaseEntity[] { databaseEntity } + }; + yield return new object[] + { + $"{nameof(DataAccess.Orm.Sql.Postgres)} - scalar result - single or default async 2", + new Func(container => container.Resolve().All().Where(it => it.BooleanField).CachedExpression(Guid.NewGuid().ToString()).SingleOrDefaultAsync(token).Result), + new Action( + (query, log) => CheckSqlCommand(query, + $@"SELECT{Environment.NewLine}{'\t'}a.""{nameof(DatabaseEntity.BooleanField)}"",{Environment.NewLine}{'\t'}a.""{nameof(DatabaseEntity.Enum)}"",{Environment.NewLine}{'\t'}a.""{nameof(DatabaseEntity.IntField)}"",{Environment.NewLine}{'\t'}a.""{nameof(DatabaseEntity.NullableStringField)}"",{Environment.NewLine}{'\t'}a.""{nameof(DatabaseEntity.PrimaryKey)}"",{Environment.NewLine}{'\t'}a.""{nameof(DatabaseEntity.StringField)}"",{Environment.NewLine}{'\t'}a.""{nameof(DatabaseEntity.Version)}""{Environment.NewLine}FROM{Environment.NewLine}{'\t'}""{schema}"".""{nameof(DatabaseEntity)}"" a{Environment.NewLine}WHERE{Environment.NewLine}{'\t'}a.""{nameof(DatabaseEntity.BooleanField)}""{Environment.NewLine}FETCH FIRST 2 ROWS ONLY", + Array.Empty(), + log)), + new IDatabaseEntity[] { databaseEntity } + }; + yield return new object[] + { + $"{nameof(DataAccess.Orm.Sql.Postgres)} - scalar result - single by primary key", + new Func(container => container.Resolve().Single(databaseEntity.PrimaryKey, token).Result), + new Action( + (query, log) => CheckSqlCommand(query, + $@"SELECT{Environment.NewLine}{'\t'}a.""{nameof(DatabaseEntity.BooleanField)}"",{Environment.NewLine}{'\t'}a.""{nameof(DatabaseEntity.Enum)}"",{Environment.NewLine}{'\t'}a.""{nameof(DatabaseEntity.IntField)}"",{Environment.NewLine}{'\t'}a.""{nameof(DatabaseEntity.NullableStringField)}"",{Environment.NewLine}{'\t'}a.""{nameof(DatabaseEntity.PrimaryKey)}"",{Environment.NewLine}{'\t'}a.""{nameof(DatabaseEntity.StringField)}"",{Environment.NewLine}{'\t'}a.""{nameof(DatabaseEntity.Version)}""{Environment.NewLine}FROM{Environment.NewLine}{'\t'}""{schema}"".""{nameof(DatabaseEntity)}"" a{Environment.NewLine}WHERE{Environment.NewLine}{'\t'}a.""{nameof(DatabaseEntity.PrimaryKey)}"" = @param_0{Environment.NewLine}FETCH FIRST 2 ROWS ONLY", + new[] { new SqlCommandParameter("param_0", databaseEntity.PrimaryKey, typeof(Guid)) }, + log)), + new IDatabaseEntity[] { databaseEntity } + }; + yield return new object[] + { + $"{nameof(DataAccess.Orm.Sql.Postgres)} - scalar result - single or default by primary key", + new Func(container => container.Resolve().SingleOrDefault(databaseEntity.PrimaryKey, token).Result), + new Action( + (query, log) => CheckSqlCommand(query, + $@"SELECT{Environment.NewLine}{'\t'}a.""{nameof(DatabaseEntity.BooleanField)}"",{Environment.NewLine}{'\t'}a.""{nameof(DatabaseEntity.Enum)}"",{Environment.NewLine}{'\t'}a.""{nameof(DatabaseEntity.IntField)}"",{Environment.NewLine}{'\t'}a.""{nameof(DatabaseEntity.NullableStringField)}"",{Environment.NewLine}{'\t'}a.""{nameof(DatabaseEntity.PrimaryKey)}"",{Environment.NewLine}{'\t'}a.""{nameof(DatabaseEntity.StringField)}"",{Environment.NewLine}{'\t'}a.""{nameof(DatabaseEntity.Version)}""{Environment.NewLine}FROM{Environment.NewLine}{'\t'}""{schema}"".""{nameof(DatabaseEntity)}"" a{Environment.NewLine}WHERE{Environment.NewLine}{'\t'}a.""{nameof(DatabaseEntity.PrimaryKey)}"" = @param_0{Environment.NewLine}FETCH FIRST 2 ROWS ONLY", + new[] { new SqlCommandParameter("param_0", databaseEntity.PrimaryKey, typeof(Guid)) }, + log)), + new IDatabaseEntity[] { databaseEntity } + }; + + /* + * Relations + */ + + yield return new object[] + { + $"{nameof(DataAccess.Orm.Sql.Postgres)} - one-to-one relation in filter", + new Func(container => container.Resolve().All().Where(it => it.Blog.Theme == "MilkyWay" && it.User.Nickname == "SpaceEngineer")), + new Action( + (query, log) => CheckSqlCommand(query, + $@"SELECT{Environment.NewLine}{'\t'}a.""{nameof(Post.Blog)}"",{Environment.NewLine}{'\t'}a.""{nameof(Post.DateTime)}"",{Environment.NewLine}{'\t'}a.""{nameof(Post.PrimaryKey)}"",{Environment.NewLine}{'\t'}a.""{nameof(Post.Text)}"",{Environment.NewLine}{'\t'}a.""{nameof(Post.User)}"",{Environment.NewLine}{'\t'}a.""{nameof(Post.Version)}""{Environment.NewLine}FROM{Environment.NewLine}{'\t'}""{schema}"".""{nameof(Blog)}"" c{Environment.NewLine}JOIN{Environment.NewLine}{'\t'}""{schema}"".""{nameof(DatabaseEntities.Relations.User)}"" b{Environment.NewLine}JOIN{Environment.NewLine}{'\t'}""{schema}"".""{nameof(Post)}"" a{Environment.NewLine}ON{Environment.NewLine}{'\t'}b.""{nameof(DatabaseEntities.Relations.User.PrimaryKey)}"" = a.""{nameof(Post.User)}""{Environment.NewLine}ON{Environment.NewLine}{'\t'}c.""{nameof(Blog.PrimaryKey)}"" = a.""{nameof(Post.Blog)}""{Environment.NewLine}WHERE{Environment.NewLine}{'\t'}CASE WHEN @param_0 IS NULL THEN c.""{nameof(Blog.Theme)}"" IS NULL ELSE c.""{nameof(Blog.Theme)}"" = @param_1 END AND CASE WHEN @param_2 IS NULL THEN b.""{nameof(DatabaseEntities.Relations.User.Nickname)}"" IS NULL ELSE b.""{nameof(DatabaseEntities.Relations.User.Nickname)}"" = @param_3 END", + new[] { new SqlCommandParameter("param_0", "MilkyWay", typeof(string)), new SqlCommandParameter("param_1", "MilkyWay", typeof(string)), new SqlCommandParameter("param_2", "SpaceEngineer", typeof(string)), new SqlCommandParameter("param_3", "SpaceEngineer", typeof(string)) }, + log)), + new IDatabaseEntity[] { user, blog, post } + }; + yield return new object[] + { + $"{nameof(DataAccess.Orm.Sql.Postgres)} - one-to-one relation in projection with filter as source", + new Func(container => container.Resolve().All().Where(it => it.DateTime > DateTime.MinValue).Select(it => new { it.Blog.Theme, Author = it.User.Nickname })), + new Action( + (query, log) => CheckSqlCommand(query, + $@"SELECT{Environment.NewLine}{'\t'}d.""{nameof(Post.Blog.Theme)}"",{Environment.NewLine}{'\t'}c.""{nameof(DatabaseEntities.Relations.User.Nickname)}"" AS ""Author""{Environment.NewLine}FROM{Environment.NewLine}{'\t'}""{schema}"".""{nameof(Blog)}"" d{Environment.NewLine}JOIN{Environment.NewLine}{'\t'}""{schema}"".""{nameof(DatabaseEntities.Relations.User)}"" c{Environment.NewLine}JOIN{Environment.NewLine}{'\t'}(SELECT{Environment.NewLine}{'\t'}{'\t'}a.""{nameof(Post.Blog)}"",{Environment.NewLine}{'\t'}{'\t'}a.""{nameof(Post.DateTime)}"",{Environment.NewLine}{'\t'}{'\t'}a.""{nameof(Post.PrimaryKey)}"",{Environment.NewLine}{'\t'}{'\t'}a.""{nameof(Post.Text)}"",{Environment.NewLine}{'\t'}{'\t'}a.""{nameof(Post.User)}"",{Environment.NewLine}{'\t'}{'\t'}a.""{nameof(Post.Version)}""{Environment.NewLine}{'\t'}FROM{Environment.NewLine}{'\t'}{'\t'}""{schema}"".""{nameof(Post)}"" a{Environment.NewLine}{'\t'}WHERE{Environment.NewLine}{'\t'}{'\t'}a.""{nameof(Post.DateTime)}"" > @param_0) b{Environment.NewLine}ON{Environment.NewLine}{'\t'}c.""{nameof(DatabaseEntities.Relations.User.PrimaryKey)}"" = b.""{nameof(Post.User)}""{Environment.NewLine}ON{Environment.NewLine}{'\t'}d.""{nameof(Blog.PrimaryKey)}"" = b.""{nameof(Post.Blog)}""", + new[] { new SqlCommandParameter("param_0", DateTime.MinValue, typeof(DateTime)) }, + log)), + new IDatabaseEntity[] { user, blog, post } + }; + yield return new object[] + { + $"{nameof(DataAccess.Orm.Sql.Postgres)} - one-to-one relation in projection", + new Func(container => container.Resolve().All().Select(it => new { it.Blog.Theme, Author = it.User.Nickname })), + new Action( + (query, log) => CheckSqlCommand(query, + $@"SELECT{Environment.NewLine}{'\t'}c.""{nameof(Post.Blog.Theme)}"",{Environment.NewLine}{'\t'}b.""{nameof(DatabaseEntities.Relations.User.Nickname)}"" AS ""Author""{Environment.NewLine}FROM{Environment.NewLine}{'\t'}""{schema}"".""{nameof(Blog)}"" c{Environment.NewLine}JOIN{Environment.NewLine}{'\t'}""{schema}"".""{nameof(DatabaseEntities.Relations.User)}"" b{Environment.NewLine}JOIN{Environment.NewLine}{'\t'}""{schema}"".""{nameof(Post)}"" a{Environment.NewLine}ON{Environment.NewLine}{'\t'}b.""{nameof(DatabaseEntities.Relations.User.PrimaryKey)}"" = a.""{nameof(Post.User)}""{Environment.NewLine}ON{Environment.NewLine}{'\t'}c.""{nameof(Blog.PrimaryKey)}"" = a.""{nameof(Post.Blog)}""", + Array.Empty(), + log)), + new IDatabaseEntity[] { user, blog, post } + }; + yield return new object[] + { + $"{nameof(DataAccess.Orm.Sql.Postgres)} - select one-to-one relation", + new Func(container => container.Resolve().All().Where(it => it.DateTime > DateTime.MinValue).Select(it => it.Blog)), + new Action( + (query, log) => CheckSqlCommand(query, + $@"SELECT{Environment.NewLine}{'\t'}c.""{nameof(Blog.PrimaryKey)}"",{Environment.NewLine}{'\t'}c.""{nameof(Blog.Theme)}"",{Environment.NewLine}{'\t'}c.""{nameof(Blog.Version)}""{Environment.NewLine}FROM{Environment.NewLine}{'\t'}""{schema}"".""{nameof(Blog)}"" c{Environment.NewLine}JOIN{Environment.NewLine}{'\t'}(SELECT{Environment.NewLine}{'\t'}{'\t'}a.""{nameof(Post.Blog)}"",{Environment.NewLine}{'\t'}{'\t'}a.""{nameof(Post.DateTime)}"",{Environment.NewLine}{'\t'}{'\t'}a.""{nameof(Post.PrimaryKey)}"",{Environment.NewLine}{'\t'}{'\t'}a.""{nameof(Post.Text)}"",{Environment.NewLine}{'\t'}{'\t'}a.""{nameof(Post.User)}"",{Environment.NewLine}{'\t'}{'\t'}a.""{nameof(Post.Version)}""{Environment.NewLine}{'\t'}FROM{Environment.NewLine}{'\t'}{'\t'}""{schema}"".""{nameof(Post)}"" a{Environment.NewLine}{'\t'}WHERE{Environment.NewLine}{'\t'}{'\t'}a.""{nameof(Post.DateTime)}"" > @param_0) b{Environment.NewLine}ON{Environment.NewLine}{'\t'}c.""{nameof(Blog.PrimaryKey)}"" = b.""{nameof(Post.Blog)}""", + new[] { new SqlCommandParameter("param_0", DateTime.MinValue, typeof(DateTime)) }, + log)), + new IDatabaseEntity[] { user, blog, post } + }; + + /* + * Order by + */ + + yield return new object[] + { + $"{nameof(DataAccess.Orm.Sql.Postgres)} - order by join", + new Func(container => container.Resolve().All().OrderByDescending(it => it.Blog.Theme).ThenBy(it => it.User.Nickname)), + new Action( + (query, log) => CheckSqlCommand(query, + $@"SELECT{Environment.NewLine}{'\t'}a.""{nameof(Post.Blog)}"",{Environment.NewLine}{'\t'}a.""{nameof(Post.DateTime)}"",{Environment.NewLine}{'\t'}a.""{nameof(Post.PrimaryKey)}"",{Environment.NewLine}{'\t'}a.""{nameof(Post.Text)}"",{Environment.NewLine}{'\t'}a.""{nameof(Post.User)}"",{Environment.NewLine}{'\t'}a.""{nameof(DatabaseEntity.Version)}""{Environment.NewLine}FROM{Environment.NewLine}{'\t'}""{schema}"".""{nameof(Blog)}"" c{Environment.NewLine}JOIN{Environment.NewLine}{'\t'}""{schema}"".""{nameof(DatabaseEntities.Relations.User)}"" b{Environment.NewLine}JOIN{Environment.NewLine}{'\t'}""{schema}"".""{nameof(Post)}"" a{Environment.NewLine}ON{Environment.NewLine}{'\t'}b.""{nameof(DatabaseEntities.Relations.User.PrimaryKey)}"" = a.""{nameof(post.User)}""{Environment.NewLine}ON{Environment.NewLine}{'\t'}c.""{nameof(Blog.PrimaryKey)}"" = a.""{nameof(post.Blog)}""{Environment.NewLine}ORDER BY{Environment.NewLine}{'\t'}c.""{nameof(Blog.Theme)}"" DESC, b.""{nameof(DatabaseEntities.Relations.User.Nickname)}"" ASC", + Array.Empty(), + log)), + new IDatabaseEntity[] { user, blog, post } + }; + yield return new object[] + { + $"{nameof(DataAccess.Orm.Sql.Postgres)} - order by then by", + new Func(container => container.Resolve().All().Where(it => it.BooleanField).OrderBy(it => it.IntField).ThenByDescending(it => it.StringField)), + new Action( + (query, log) => CheckSqlCommand(query, + $@"SELECT{Environment.NewLine}{'\t'}a.""{nameof(DatabaseEntity.BooleanField)}"",{Environment.NewLine}{'\t'}a.""{nameof(DatabaseEntity.Enum)}"",{Environment.NewLine}{'\t'}a.""{nameof(DatabaseEntity.IntField)}"",{Environment.NewLine}{'\t'}a.""{nameof(DatabaseEntity.NullableStringField)}"",{Environment.NewLine}{'\t'}a.""{nameof(DatabaseEntity.PrimaryKey)}"",{Environment.NewLine}{'\t'}a.""{nameof(DatabaseEntity.StringField)}"",{Environment.NewLine}{'\t'}a.""{nameof(DatabaseEntity.Version)}""{Environment.NewLine}FROM{Environment.NewLine}{'\t'}""{schema}"".""{nameof(DatabaseEntity)}"" a{Environment.NewLine}WHERE{Environment.NewLine}{'\t'}a.""{nameof(DatabaseEntity.BooleanField)}""{Environment.NewLine}ORDER BY{Environment.NewLine}{'\t'}a.""{nameof(DatabaseEntity.IntField)}"" ASC, a.""{nameof(DatabaseEntity.StringField)}"" DESC", + Array.Empty(), + log)), + new IDatabaseEntity[] { databaseEntity } + }; + yield return new object[] + { + $"{nameof(DataAccess.Orm.Sql.Postgres)} - order by", + new Func(container => container.Resolve().All().Where(it => it.BooleanField).OrderBy(it => it.IntField)), + new Action( + (query, log) => CheckSqlCommand(query, + $@"SELECT{Environment.NewLine}{'\t'}a.""{nameof(DatabaseEntity.BooleanField)}"",{Environment.NewLine}{'\t'}a.""{nameof(DatabaseEntity.Enum)}"",{Environment.NewLine}{'\t'}a.""{nameof(DatabaseEntity.IntField)}"",{Environment.NewLine}{'\t'}a.""{nameof(DatabaseEntity.NullableStringField)}"",{Environment.NewLine}{'\t'}a.""{nameof(DatabaseEntity.PrimaryKey)}"",{Environment.NewLine}{'\t'}a.""{nameof(DatabaseEntity.StringField)}"",{Environment.NewLine}{'\t'}a.""{nameof(DatabaseEntity.Version)}""{Environment.NewLine}FROM{Environment.NewLine}{'\t'}""{schema}"".""{nameof(DatabaseEntity)}"" a{Environment.NewLine}WHERE{Environment.NewLine}{'\t'}a.""{nameof(DatabaseEntity.BooleanField)}""{Environment.NewLine}ORDER BY{Environment.NewLine}{'\t'}a.""{nameof(DatabaseEntity.IntField)}"" ASC", + Array.Empty(), + log)), + new IDatabaseEntity[] { databaseEntity } + }; + + /* + * Sub query + */ + + yield return new object[] + { + $"{nameof(DataAccess.Orm.Sql.Postgres)} - sub-query", + new Func(container => + { + var subQuery = container.Resolve().All().Select(it => it.PrimaryKey); + return container.Resolve().All().Where(it => subQuery.Contains(it.PrimaryKey)); + }), + new Action( + (query, log) => CheckSqlCommand(query, + $@"SELECT{Environment.NewLine}{'\t'}a.""{nameof(DatabaseEntity.BooleanField)}"",{Environment.NewLine}{'\t'}a.""{nameof(DatabaseEntity.Enum)}"",{Environment.NewLine}{'\t'}a.""{nameof(DatabaseEntity.IntField)}"",{Environment.NewLine}{'\t'}a.""{nameof(DatabaseEntity.NullableStringField)}"",{Environment.NewLine}{'\t'}a.""{nameof(DatabaseEntity.PrimaryKey)}"",{Environment.NewLine}{'\t'}a.""{nameof(DatabaseEntity.StringField)}"",{Environment.NewLine}{'\t'}a.""{nameof(DatabaseEntity.Version)}""{Environment.NewLine}FROM{Environment.NewLine}{'\t'}""{schema}"".""{nameof(DatabaseEntity)}"" a{Environment.NewLine}WHERE{Environment.NewLine}{'\t'}a.""{nameof(DatabaseEntity.PrimaryKey)}"" = ANY(SELECT{Environment.NewLine}{'\t'}{'\t'}a.""{nameof(DatabaseEntity.PrimaryKey)}""{Environment.NewLine}{'\t'}FROM{Environment.NewLine}{'\t'}{'\t'}""{schema}"".""{nameof(DatabaseEntity)}"" a)", + Array.Empty(), + log)), + new IDatabaseEntity[] { databaseEntity } + }; + yield return new object[] + { + $"{nameof(DataAccess.Orm.Sql.Postgres)} - sub-query with parameters", + new Func(container => + { + var subQuery = container.Resolve().All().Where(it => it.BooleanField == true).Select(it => it.PrimaryKey); + return container.Resolve().All().Where(it => it.NullableStringField != null && subQuery.Contains(it.PrimaryKey)); + }), + new Action( + (query, log) => CheckSqlCommand(query, + $@"SELECT{Environment.NewLine}{'\t'}a.""{nameof(DatabaseEntity.BooleanField)}"",{Environment.NewLine}{'\t'}a.""{nameof(DatabaseEntity.Enum)}"",{Environment.NewLine}{'\t'}a.""{nameof(DatabaseEntity.IntField)}"",{Environment.NewLine}{'\t'}a.""{nameof(DatabaseEntity.NullableStringField)}"",{Environment.NewLine}{'\t'}a.""{nameof(DatabaseEntity.PrimaryKey)}"",{Environment.NewLine}{'\t'}a.""{nameof(DatabaseEntity.StringField)}"",{Environment.NewLine}{'\t'}a.""{nameof(DatabaseEntity.Version)}""{Environment.NewLine}FROM{Environment.NewLine}{'\t'}""{schema}"".""{nameof(DatabaseEntity)}"" a{Environment.NewLine}WHERE{Environment.NewLine}{'\t'}CASE WHEN @param_0 IS NULL THEN a.""{nameof(DatabaseEntity.NullableStringField)}"" IS NOT NULL ELSE a.""{nameof(DatabaseEntity.NullableStringField)}"" != @param_1 END AND a.""{nameof(DatabaseEntity.PrimaryKey)}"" = ANY(SELECT{Environment.NewLine}{'\t'}{'\t'}{'\t'}a.""{nameof(DatabaseEntity.PrimaryKey)}""{Environment.NewLine}{'\t'}{'\t'}FROM{Environment.NewLine}{'\t'}{'\t'}{'\t'}(SELECT{Environment.NewLine}{'\t'}{'\t'}{'\t'}{'\t'}a.""{nameof(DatabaseEntity.BooleanField)}"",{Environment.NewLine}{'\t'}{'\t'}{'\t'}{'\t'}a.""{nameof(DatabaseEntity.Enum)}"",{Environment.NewLine}{'\t'}{'\t'}{'\t'}{'\t'}a.""{nameof(DatabaseEntity.IntField)}"",{Environment.NewLine}{'\t'}{'\t'}{'\t'}{'\t'}a.""{nameof(DatabaseEntity.NullableStringField)}"",{Environment.NewLine}{'\t'}{'\t'}{'\t'}{'\t'}a.""{nameof(DatabaseEntity.PrimaryKey)}"",{Environment.NewLine}{'\t'}{'\t'}{'\t'}{'\t'}a.""{nameof(DatabaseEntity.StringField)}"",{Environment.NewLine}{'\t'}{'\t'}{'\t'}{'\t'}a.""{nameof(DatabaseEntity.Version)}""{Environment.NewLine}{'\t'}{'\t'}{'\t'}FROM{Environment.NewLine}{'\t'}{'\t'}{'\t'}{'\t'}""{schema}"".""{nameof(DatabaseEntity)}"" a{Environment.NewLine}{'\t'}{'\t'}{'\t'}WHERE{Environment.NewLine}{'\t'}{'\t'}{'\t'}{'\t'}a.""{nameof(DatabaseEntity.BooleanField)}"" = @param_2) b)", + new[] { new SqlCommandParameter("param_0", default(string), typeof(string)), new SqlCommandParameter("param_1", default(string), typeof(string)), new SqlCommandParameter("param_2", true, typeof(bool)) }, + log)), + new IDatabaseEntity[] { databaseEntity } + }; + + /* + * Sql view + */ + + yield return new object[] + { + $"{nameof(DataAccess.Orm.Sql.Postgres)} - sql view translation after migration", + new Func(container => container.Resolve().All().Where(column => column.Schema == schema).First()), + new Action( + (query, log) => CheckSqlCommand(query, + $@"SELECT{Environment.NewLine}{'\t'}a.""{nameof(DatabaseColumn.Column)}"",{Environment.NewLine}{'\t'}a.""{nameof(DatabaseColumn.DataType)}"",{Environment.NewLine}{'\t'}a.""{nameof(DatabaseColumn.DefaultValue)}"",{Environment.NewLine}{'\t'}a.""{nameof(DatabaseColumn.Length)}"",{Environment.NewLine}{'\t'}a.""{nameof(DatabaseColumn.Nullable)}"",{Environment.NewLine}{'\t'}a.""{nameof(DatabaseColumn.Position)}"",{Environment.NewLine}{'\t'}a.""{nameof(DatabaseColumn.Precision)}"",{Environment.NewLine}{'\t'}a.""{nameof(DatabaseColumn.PrimaryKey)}"",{Environment.NewLine}{'\t'}a.""{nameof(DatabaseColumn.Scale)}"",{Environment.NewLine}{'\t'}a.""{nameof(DatabaseColumn.Schema)}"",{Environment.NewLine}{'\t'}a.""{nameof(DatabaseColumn.Table)}""{Environment.NewLine}FROM{Environment.NewLine}{'\t'}""{nameof(DataAccess.Orm.Sql.Migrations)}"".""{nameof(DatabaseColumn)}"" a{Environment.NewLine}WHERE{Environment.NewLine}{'\t'}CASE WHEN @param_0 IS NULL THEN a.""{nameof(DatabaseColumn.Schema)}"" IS NULL ELSE a.""{nameof(DatabaseColumn.Schema)}"" = @param_1 END{Environment.NewLine}FETCH FIRST 1 ROWS ONLY", + new[] { new SqlCommandParameter("param_0", schema, typeof(string)), new SqlCommandParameter("param_1", schema, typeof(string)) }, + log)), + Array.Empty() + }; + yield return new object[] + { + $"{nameof(DataAccess.Orm.Sql.Postgres)} - sql view translation before migration (cachekey = 'C3B9DD2E-7279-455D-A718-356FD8F86035')", + new Func(container => container.Resolve().All().Where(column => column.Schema == schema).CachedExpression("C3B9DD2E-7279-455D-A718-356FD8F86035").ToListAsync(token).Result.First()), + new Action( + (query, log) => CheckSqlCommand(query, + $@"SELECT{Environment.NewLine}{'\t'}a.""{nameof(DatabaseColumn.Column)}"",{Environment.NewLine}{'\t'}a.""{nameof(DatabaseColumn.DataType)}"",{Environment.NewLine}{'\t'}a.""{nameof(DatabaseColumn.DefaultValue)}"",{Environment.NewLine}{'\t'}a.""{nameof(DatabaseColumn.Length)}"",{Environment.NewLine}{'\t'}a.""{nameof(DatabaseColumn.Nullable)}"",{Environment.NewLine}{'\t'}a.""{nameof(DatabaseColumn.Position)}"",{Environment.NewLine}{'\t'}a.""{nameof(DatabaseColumn.Precision)}"",{Environment.NewLine}{'\t'}a.""{nameof(DatabaseColumn.PrimaryKey)}"",{Environment.NewLine}{'\t'}a.""{nameof(DatabaseColumn.Scale)}"",{Environment.NewLine}{'\t'}a.""{nameof(DatabaseColumn.Schema)}"",{Environment.NewLine}{'\t'}a.""{nameof(DatabaseColumn.Table)}""{Environment.NewLine}FROM{Environment.NewLine}{'\t'}({Environment.NewLine}{'\t'}{'\t'}select{Environment.NewLine}{'\t'}{'\t'}gen_random_uuid() as ""{nameof(DatabaseColumn.PrimaryKey)}"",{Environment.NewLine}{'\t'}{'\t'}c.table_schema as ""{nameof(DatabaseColumn.Schema)}"",{Environment.NewLine}{'\t'}{'\t'}c.table_name as ""{nameof(DatabaseColumn.Table)}"",{Environment.NewLine}{'\t'}{'\t'}column_name as ""{nameof(DatabaseColumn.Column)}"",{Environment.NewLine}{'\t'}{'\t'}ordinal_position as ""{nameof(DatabaseColumn.Position)}"",{Environment.NewLine}{'\t'}{'\t'}data_type as ""{nameof(DatabaseColumn.DataType)}"",{Environment.NewLine}{'\t'}{'\t'}case is_nullable when 'NO' then false when 'YES' then true end as ""{nameof(DatabaseColumn.Nullable)}"",{Environment.NewLine}{'\t'}{'\t'}column_default as ""{nameof(DatabaseColumn.DefaultValue)}"",{Environment.NewLine}{'\t'}{'\t'}numeric_scale as ""{nameof(DatabaseColumn.Scale)}"",{Environment.NewLine}{'\t'}{'\t'}numeric_precision as ""{nameof(DatabaseColumn.Precision)}"",{Environment.NewLine}{'\t'}{'\t'}character_maximum_length as ""{nameof(DatabaseColumn.Length)}""{Environment.NewLine}{'\t'}{'\t'}from information_schema.columns c{Environment.NewLine}{'\t'}{'\t'}join information_schema.tables t{Environment.NewLine}{'\t'}{'\t'}on t.table_schema = c.table_schema and t.table_name = c.table_name {Environment.NewLine}{'\t'}{'\t'}where t.table_type != 'VIEW' and c.table_schema not in ('information_schema', 'public') and c.table_schema not like 'pg_%'{Environment.NewLine}{'\t'}{'\t'}order by c.table_name, ordinal_position{Environment.NewLine}{'\t'}) a{Environment.NewLine}WHERE{Environment.NewLine}{'\t'}CASE WHEN @param_0 IS NULL THEN a.""{nameof(DatabaseColumn.Schema)}"" IS NULL ELSE a.""{nameof(DatabaseColumn.Schema)}"" = @param_1 END", + new[] { new SqlCommandParameter("param_0", schema, typeof(string)), new SqlCommandParameter("param_1", schema, typeof(string)) }, + log)), + Array.Empty() + }; + + /* + * Json + */ + + yield return new object[] + { + $"{nameof(DataAccess.Orm.Sql.Postgres)} - select json column", + new Func(container => container.Resolve().All().Where(it => it.AggregateId == aggregateId).Select(it => it.DomainEvent)), + new Action( + (query, log) => CheckSqlCommand(query, + $@"SELECT{Environment.NewLine}{'\t'}b.""{nameof(DatabaseDomainEvent.DomainEvent)}""{Environment.NewLine}FROM{Environment.NewLine}{'\t'}(SELECT{Environment.NewLine}{'\t'}{'\t'}a.""{nameof(DatabaseDomainEvent.AggregateId)}"",{Environment.NewLine}{'\t'}{'\t'}a.""{nameof(DatabaseDomainEvent.DomainEvent)}"",{Environment.NewLine}{'\t'}{'\t'}a.""{nameof(DatabaseDomainEvent.Index)}"",{Environment.NewLine}{'\t'}{'\t'}a.""{nameof(DatabaseDomainEvent.PrimaryKey)}"",{Environment.NewLine}{'\t'}{'\t'}a.""{nameof(DatabaseDomainEvent.Timestamp)}"",{Environment.NewLine}{'\t'}{'\t'}a.""{nameof(DatabaseDomainEvent.Version)}""{Environment.NewLine}{'\t'}FROM{Environment.NewLine}{'\t'}{'\t'}""{nameof(GenericEndpoint.EventSourcing)}"".""{nameof(DatabaseDomainEvent)}"" a{Environment.NewLine}{'\t'}WHERE{Environment.NewLine}{'\t'}{'\t'}a.""{nameof(DatabaseDomainEvent.AggregateId)}"" = @param_0) b", + new[] { new SqlCommandParameter("param_0", aggregateId, typeof(Guid)) }, + log)), + new IDatabaseEntity[] { domainEvent } + }; + yield return new object[] + { + $"{nameof(DataAccess.Orm.Sql.Postgres)} - select json column to anonymous type", + new Func(container => container.Resolve().All().Where(it => it.AggregateId == aggregateId).Select(it => new { it.AggregateId, it.DomainEvent })), + new Action( + (query, log) => CheckSqlCommand(query, + $@"SELECT{Environment.NewLine}{'\t'}b.""{nameof(DatabaseDomainEvent.AggregateId)}"",{Environment.NewLine}{'\t'}b.""{nameof(DatabaseDomainEvent.DomainEvent)}""{Environment.NewLine}FROM{Environment.NewLine}{'\t'}(SELECT{Environment.NewLine}{'\t'}{'\t'}a.""{nameof(DatabaseDomainEvent.AggregateId)}"",{Environment.NewLine}{'\t'}{'\t'}a.""{nameof(DatabaseDomainEvent.DomainEvent)}"",{Environment.NewLine}{'\t'}{'\t'}a.""{nameof(DatabaseDomainEvent.Index)}"",{Environment.NewLine}{'\t'}{'\t'}a.""{nameof(DatabaseDomainEvent.PrimaryKey)}"",{Environment.NewLine}{'\t'}{'\t'}a.""{nameof(DatabaseDomainEvent.Timestamp)}"",{Environment.NewLine}{'\t'}{'\t'}a.""{nameof(DatabaseDomainEvent.Version)}""{Environment.NewLine}{'\t'}FROM{Environment.NewLine}{'\t'}{'\t'}""{nameof(GenericEndpoint.EventSourcing)}"".""{nameof(DatabaseDomainEvent)}"" a{Environment.NewLine}{'\t'}WHERE{Environment.NewLine}{'\t'}{'\t'}a.""{nameof(DatabaseDomainEvent.AggregateId)}"" = @param_0) b", + new[] { new SqlCommandParameter("param_0", aggregateId, typeof(Guid)) }, + log)), + new IDatabaseEntity[] { domainEvent } + }; + yield return new object[] + { + $"{nameof(DataAccess.Orm.Sql.Postgres)} - select json attribute", + new Func(container => container.Resolve().All().Where(it => it.DomainEvent.AsJsonObject().HasJsonAttribute(nameof(UserWasCreated.Username)) && it.DomainEvent.AsJsonObject().GetJsonAttribute(nameof(UserWasCreated.Username)).HasJsonAttribute(nameof(Username.Value)) && it.DomainEvent.AsJsonObject().GetJsonAttribute(nameof(UserWasCreated.Username)).GetJsonAttribute(nameof(Username.Value)) == username.AsJsonObject()).Select(it => it.DomainEvent.AsJsonObject().GetJsonAttribute(nameof(UserWasCreated.Username)).GetJsonAttribute(nameof(Username.Value)).Value)), + new Action( + (query, log) => CheckSqlCommand(query, + $@"SELECT{Environment.NewLine}{'\t'}(CASE WHEN SUBSTRING(LTRIM((CASE WHEN SUBSTRING(LTRIM(b.""{nameof(DatabaseDomainEvent.DomainEvent)}""[@param_6]::text), 1, 1) = '{{' THEN b.""{nameof(DatabaseDomainEvent.DomainEvent)}""[@param_6] - array(SELECT * from jsonb_object_keys(b.""{nameof(DatabaseDomainEvent.DomainEvent)}""[@param_6]) WHERE jsonb_object_keys like '$%') ELSE b.""{nameof(DatabaseDomainEvent.DomainEvent)}""[@param_6] END)[@param_7]::text), 1, 1) = '{{' THEN (CASE WHEN SUBSTRING(LTRIM(b.""{nameof(DatabaseDomainEvent.DomainEvent)}""[@param_6]::text), 1, 1) = '{{' THEN b.""{nameof(DatabaseDomainEvent.DomainEvent)}""[@param_6] - array(SELECT * from jsonb_object_keys(b.""{nameof(DatabaseDomainEvent.DomainEvent)}""[@param_6]) WHERE jsonb_object_keys like '$%') ELSE b.""{nameof(DatabaseDomainEvent.DomainEvent)}""[@param_6] END)[@param_7] - array(SELECT * from jsonb_object_keys((CASE WHEN SUBSTRING(LTRIM(b.""{nameof(DatabaseDomainEvent.DomainEvent)}""[@param_6]::text), 1, 1) = '{{' THEN b.""{nameof(DatabaseDomainEvent.DomainEvent)}""[@param_6] - array(SELECT * from jsonb_object_keys(b.""{nameof(DatabaseDomainEvent.DomainEvent)}""[@param_6]) WHERE jsonb_object_keys like '$%') ELSE b.""{nameof(DatabaseDomainEvent.DomainEvent)}""[@param_6] END)[@param_7]) WHERE jsonb_object_keys like '$%') ELSE (CASE WHEN SUBSTRING(LTRIM(b.""{nameof(DatabaseDomainEvent.DomainEvent)}""[@param_6]::text), 1, 1) = '{{' THEN b.""{nameof(DatabaseDomainEvent.DomainEvent)}""[@param_6] - array(SELECT * from jsonb_object_keys(b.""{nameof(DatabaseDomainEvent.DomainEvent)}""[@param_6]) WHERE jsonb_object_keys like '$%') ELSE b.""{nameof(DatabaseDomainEvent.DomainEvent)}""[@param_6] END)[@param_7] END){Environment.NewLine}FROM{Environment.NewLine}{'\t'}(SELECT{Environment.NewLine}{'\t'}{'\t'}a.""{nameof(DatabaseDomainEvent.AggregateId)}"",{Environment.NewLine}{'\t'}{'\t'}a.""{nameof(DatabaseDomainEvent.DomainEvent)}"",{Environment.NewLine}{'\t'}{'\t'}a.""{nameof(DatabaseDomainEvent.Index)}"",{Environment.NewLine}{'\t'}{'\t'}a.""{nameof(DatabaseDomainEvent.PrimaryKey)}"",{Environment.NewLine}{'\t'}{'\t'}a.""{nameof(DatabaseDomainEvent.Timestamp)}"",{Environment.NewLine}{'\t'}{'\t'}a.""{nameof(DatabaseDomainEvent.Version)}""{Environment.NewLine}{'\t'}FROM{Environment.NewLine}{'\t'}{'\t'}""{nameof(GenericEndpoint.EventSourcing)}"".""{nameof(DatabaseDomainEvent)}"" a{Environment.NewLine}{'\t'}WHERE{Environment.NewLine}{'\t'}{'\t'}a.""{nameof(DatabaseDomainEvent.DomainEvent)}"" ? @param_0 AND (CASE WHEN SUBSTRING(LTRIM(a.""{nameof(DatabaseDomainEvent.DomainEvent)}""[@param_1]::text), 1, 1) = '{{' THEN a.""{nameof(DatabaseDomainEvent.DomainEvent)}""[@param_1] - array(SELECT * from jsonb_object_keys(a.""{nameof(DatabaseDomainEvent.DomainEvent)}""[@param_1]) WHERE jsonb_object_keys like '$%') ELSE a.""{nameof(DatabaseDomainEvent.DomainEvent)}""[@param_1] END) ? @param_2 AND (CASE WHEN SUBSTRING(LTRIM((CASE WHEN SUBSTRING(LTRIM(a.""{nameof(DatabaseDomainEvent.DomainEvent)}""[@param_3]::text), 1, 1) = '{{' THEN a.""{nameof(DatabaseDomainEvent.DomainEvent)}""[@param_3] - array(SELECT * from jsonb_object_keys(a.""{nameof(DatabaseDomainEvent.DomainEvent)}""[@param_3]) WHERE jsonb_object_keys like '$%') ELSE a.""{nameof(DatabaseDomainEvent.DomainEvent)}""[@param_3] END)[@param_4]::text), 1, 1) = '{{' THEN (CASE WHEN SUBSTRING(LTRIM(a.""{nameof(DatabaseDomainEvent.DomainEvent)}""[@param_3]::text), 1, 1) = '{{' THEN a.""{nameof(DatabaseDomainEvent.DomainEvent)}""[@param_3] - array(SELECT * from jsonb_object_keys(a.""{nameof(DatabaseDomainEvent.DomainEvent)}""[@param_3]) WHERE jsonb_object_keys like '$%') ELSE a.""{nameof(DatabaseDomainEvent.DomainEvent)}""[@param_3] END)[@param_4] - array(SELECT * from jsonb_object_keys((CASE WHEN SUBSTRING(LTRIM(a.""{nameof(DatabaseDomainEvent.DomainEvent)}""[@param_3]::text), 1, 1) = '{{' THEN a.""{nameof(DatabaseDomainEvent.DomainEvent)}""[@param_3] - array(SELECT * from jsonb_object_keys(a.""{nameof(DatabaseDomainEvent.DomainEvent)}""[@param_3]) WHERE jsonb_object_keys like '$%') ELSE a.""{nameof(DatabaseDomainEvent.DomainEvent)}""[@param_3] END)[@param_4]) WHERE jsonb_object_keys like '$%') ELSE (CASE WHEN SUBSTRING(LTRIM(a.""{nameof(DatabaseDomainEvent.DomainEvent)}""[@param_3]::text), 1, 1) = '{{' THEN a.""{nameof(DatabaseDomainEvent.DomainEvent)}""[@param_3] - array(SELECT * from jsonb_object_keys(a.""{nameof(DatabaseDomainEvent.DomainEvent)}""[@param_3]) WHERE jsonb_object_keys like '$%') ELSE a.""{nameof(DatabaseDomainEvent.DomainEvent)}""[@param_3] END)[@param_4] END) = @param_5) b", + new[] { new SqlCommandParameter("param_0", nameof(UserWasCreated.Username), typeof(string)), new SqlCommandParameter("param_1", nameof(UserWasCreated.Username), typeof(string)), new SqlCommandParameter("param_2", nameof(Username.Value), typeof(string)), new SqlCommandParameter("param_3", nameof(UserWasCreated.Username), typeof(string)), new SqlCommandParameter("param_4", nameof(Username.Value), typeof(string)), new SqlCommandParameter("param_5", username.AsJsonObject(), typeof(DatabaseJsonObject)), new SqlCommandParameter("param_6", nameof(UserWasCreated.Username), typeof(string)), new SqlCommandParameter("param_7", nameof(Username.Value), typeof(string)) }, + log)), + new IDatabaseEntity[] { domainEvent } + }; + yield return new object[] + { + $"{nameof(DataAccess.Orm.Sql.Postgres)} - compose json object", + new Func(container => container.Resolve().All().Select(it => it.DomainEvent.AsJsonObject().ExcludeJsonAttribute("$id").ExcludeJsonAttribute("$type").ExcludeJsonAttribute(nameof(UserWasCreated.Salt)).ExcludeJsonAttribute(nameof(UserWasCreated.PasswordHash)).ExcludeJsonAttribute(nameof(UserWasCreated.Username)).ConcatJsonObjects(it.DomainEvent.AsJsonObject().GetJsonAttribute(nameof(UserWasCreated.Username))).TypedValue)), + new Action( + (query, log) => CheckSqlCommand(query, + $@"SELECT{Environment.NewLine}{'\t'}((((((a.""{nameof(DatabaseDomainEvent.DomainEvent)}"" - @param_0) - @param_1) - @param_2) - @param_3) - @param_4) || (CASE WHEN SUBSTRING(LTRIM(a.""{nameof(DatabaseDomainEvent.DomainEvent)}""[@param_5]::text), 1, 1) = '{{' THEN a.""{nameof(DatabaseDomainEvent.DomainEvent)}""[@param_5] - array(SELECT * from jsonb_object_keys(a.""{nameof(DatabaseDomainEvent.DomainEvent)}""[@param_5]) WHERE jsonb_object_keys like '$%') ELSE a.""{nameof(DatabaseDomainEvent.DomainEvent)}""[@param_5] END)){Environment.NewLine}FROM{Environment.NewLine}{'\t'}""{nameof(GenericEndpoint.EventSourcing)}"".""{nameof(DatabaseDomainEvent)}"" a", + new[] { new SqlCommandParameter("param_0", "$id", typeof(string)), new SqlCommandParameter("param_1", "$type", typeof(string)), new SqlCommandParameter("param_2", nameof(UserWasCreated.Salt), typeof(string)), new SqlCommandParameter("param_3", nameof(UserWasCreated.PasswordHash), typeof(string)), new SqlCommandParameter("param_4", nameof(UserWasCreated.Username), typeof(string)), new SqlCommandParameter("param_5", nameof(UserWasCreated.Username), typeof(string)) }, + log)), + new IDatabaseEntity[] { domainEvent } + }; + + /* + * Insert + */ + + yield return new object[] + { + $"{nameof(DataAccess.Orm.Sql.Postgres)} - insert entity", + new Func(container => container.Resolve().Insert(new IDatabaseEntity[] { databaseEntity }, EnInsertBehavior.Default).CachedExpression(Guid.NewGuid().ToString()).Invoke(token).Result), + new Action( + (query, log) => CheckSqlCommand(query, + $@"INSERT INTO ""{schema}"".""{nameof(DatabaseEntity)}"" (""{nameof(DatabaseEntity.BooleanField)}"", ""{nameof(DatabaseEntity.Enum)}"", ""{nameof(DatabaseEntity.IntField)}"", ""{nameof(DatabaseEntity.NullableStringField)}"", ""{nameof(DatabaseEntity.PrimaryKey)}"", ""{nameof(DatabaseEntity.StringField)}"", ""{nameof(DatabaseEntity.Version)}""){Environment.NewLine}VALUES{Environment.NewLine}(@param_0, @param_1, @param_2, @param_3, @param_4, @param_5, @param_6)", + new[] { new SqlCommandParameter("param_0", true, typeof(bool)), new SqlCommandParameter("param_1", EnEnum.Three, typeof(EnEnum)), new SqlCommandParameter("param_2", 42, typeof(int)), new SqlCommandParameter("param_3", "SomeNullableString", typeof(string)), new SqlCommandParameter("param_4", databaseEntity.PrimaryKey, typeof(Guid)), new SqlCommandParameter("param_5", "SomeString", typeof(string)), new SqlCommandParameter("param_6", 0L, typeof(long)) }, + log)), + Array.Empty() + }; + yield return new object[] + { + $"{nameof(DataAccess.Orm.Sql.Postgres)} - insert several vales", + new Func(container => container.Resolve().Insert(new IDatabaseEntity[] { databaseEntity, DatabaseEntity.Generate(aggregateId) }, EnInsertBehavior.Default).CachedExpression(Guid.NewGuid().ToString()).Invoke(token).Result), + new Action( + (query, log) => CheckSqlCommand(query, + $@"INSERT INTO ""{schema}"".""{nameof(DatabaseEntity)}"" (""{nameof(DatabaseEntity.BooleanField)}"", ""{nameof(DatabaseEntity.Enum)}"", ""{nameof(DatabaseEntity.IntField)}"", ""{nameof(DatabaseEntity.NullableStringField)}"", ""{nameof(DatabaseEntity.PrimaryKey)}"", ""{nameof(DatabaseEntity.StringField)}"", ""{nameof(DatabaseEntity.Version)}""){Environment.NewLine}VALUES{Environment.NewLine}(@param_0, @param_1, @param_2, @param_3, @param_4, @param_5, @param_6),{Environment.NewLine}(@param_7, @param_8, @param_9, @param_10, @param_11, @param_12, @param_13)", + new[] { new SqlCommandParameter("param_0", true, typeof(bool)), new SqlCommandParameter("param_1", EnEnum.Three, typeof(EnEnum)), new SqlCommandParameter("param_2", 42, typeof(int)), new SqlCommandParameter("param_3", "SomeNullableString", typeof(string)), new SqlCommandParameter("param_4", databaseEntity.PrimaryKey, typeof(Guid)), new SqlCommandParameter("param_5", "SomeString", typeof(string)), new SqlCommandParameter("param_6", 0L, typeof(long)), new SqlCommandParameter("param_7", true, typeof(bool)), new SqlCommandParameter("param_8", EnEnum.Three, typeof(EnEnum)), new SqlCommandParameter("param_9", 42, typeof(int)), new SqlCommandParameter("param_10", "SomeNullableString", typeof(string)), new SqlCommandParameter("param_11", aggregateId, typeof(Guid)), new SqlCommandParameter("param_12", "SomeString", typeof(string)), new SqlCommandParameter("param_13", 0L, typeof(long)) }, + log)), + Array.Empty() + }; + yield return new object[] + { + $"{nameof(DataAccess.Orm.Sql.Postgres)} - insert entities", + new Func(container => container.Resolve().Insert(new IDatabaseEntity[] { user, blog, post }, EnInsertBehavior.Default).CachedExpression(Guid.NewGuid().ToString()).Invoke(token).Result), + new Action( + (query, log) => CheckSqlCommand(query, + $@"INSERT INTO ""{schema}"".""{nameof(DatabaseEntities.Relations.User)}"" (""{nameof(DatabaseEntities.Relations.User.Nickname)}"", ""{nameof(DatabaseEntities.Relations.User.PrimaryKey)}"", ""{nameof(DatabaseEntities.Relations.User.Version)}""){Environment.NewLine}VALUES{Environment.NewLine}(@param_0, @param_1, @param_2);{Environment.NewLine}INSERT INTO ""{schema}"".""{nameof(Blog)}"" (""{nameof(Blog.PrimaryKey)}"", ""{nameof(Blog.Theme)}"", ""{nameof(Blog.Version)}""){Environment.NewLine}VALUES{Environment.NewLine}(@param_3, @param_4, @param_5);{Environment.NewLine}INSERT INTO ""{schema}"".""{nameof(Post)}"" (""{nameof(Post.Blog)}"", ""{nameof(Post.DateTime)}"", ""{nameof(Post.PrimaryKey)}"", ""{nameof(Post.Text)}"", ""{nameof(Post.User)}"", ""{nameof(Post.Version)}""){Environment.NewLine}VALUES{Environment.NewLine}(@param_6, @param_7, @param_8, @param_9, @param_10, @param_11);{Environment.NewLine}INSERT INTO ""{schema}"".""{nameof(Blog)}_{nameof(Post)}"" (""{nameof(BaseMtmDatabaseEntity.Left)}"", ""{nameof(BaseMtmDatabaseEntity.Right)}""){Environment.NewLine}VALUES{Environment.NewLine}(@param_12, @param_13)", + new[] { new SqlCommandParameter("param_0", "SpaceEngineer", typeof(string)), new SqlCommandParameter("param_1", user.PrimaryKey, typeof(Guid)), new SqlCommandParameter("param_2", 0L, typeof(long)), new SqlCommandParameter("param_3", blog.PrimaryKey, typeof(Guid)), new SqlCommandParameter("param_4", blog.Theme, typeof(string)), new SqlCommandParameter("param_5", 0L, typeof(long)), new SqlCommandParameter("param_6", blog.PrimaryKey, typeof(Guid)), new SqlCommandParameter("param_7", post.DateTime, typeof(DateTime)), new SqlCommandParameter("param_8", post.PrimaryKey, typeof(Guid)), new SqlCommandParameter("param_9", post.Text, typeof(string)), new SqlCommandParameter("param_10", user.PrimaryKey, typeof(Guid)), new SqlCommandParameter("param_11", 0L, typeof(long)), new SqlCommandParameter("param_12", blog.PrimaryKey, typeof(Guid)), new SqlCommandParameter("param_13", post.PrimaryKey, typeof(Guid)) }, + log)), + Array.Empty() + }; + + /* + * Delete + */ + + yield return new object[] + { + $"{nameof(DataAccess.Orm.Sql.Postgres)} - delete entity", + new Func(container => container.Resolve().Delete().Where(it => it.BooleanField).CachedExpression(Guid.NewGuid().ToString()).Invoke(token).Result), + new Action( + (query, log) => CheckSqlCommand(query, + $@"DELETE FROM ""{schema}"".""{nameof(DatabaseEntity)}""{Environment.NewLine}WHERE{Environment.NewLine}{'\t'}""{nameof(DatabaseEntity.BooleanField)}""", + Array.Empty(), + log)), + new IDatabaseEntity[] { databaseEntity } + }; + yield return new object[] + { + $"{nameof(DataAccess.Orm.Sql.Postgres)} - delete entity with query parameters", + new Func(container => container.Resolve().Delete().Where(it => it.BooleanField == true && it.IntField == 42).CachedExpression(Guid.NewGuid().ToString()).Invoke(token).Result), + new Action( + (query, log) => CheckSqlCommand(query, + $@"DELETE FROM ""{schema}"".""{nameof(DatabaseEntity)}""{Environment.NewLine}WHERE{Environment.NewLine}{'\t'}""{nameof(DatabaseEntity.BooleanField)}"" = @param_0 AND ""{nameof(DatabaseEntity.IntField)}"" = @param_1", + new[] { new SqlCommandParameter("param_0", true, typeof(bool)), new SqlCommandParameter("param_1", 42, typeof(int)) }, + log)), + new IDatabaseEntity[] { databaseEntity } + }; + + /* + * Update + */ + + yield return new object[] + { + $"{nameof(DataAccess.Orm.Sql.Postgres)} - update entity with column reference", + new Func(container => container.Resolve().Update().Set(it => it.IntField.Assign(it.IntField + 1)).Where(it => it.BooleanField).CachedExpression(Guid.NewGuid().ToString()).Invoke(token).Result), + new Action( + (query, log) => CheckSqlCommand(query, + $@"UPDATE ""{schema}"".""{nameof(DatabaseEntity)}""{Environment.NewLine}SET ""{nameof(DatabaseEntity.IntField)}"" = ""{nameof(DatabaseEntity.IntField)}"" + @param_0{Environment.NewLine}WHERE{Environment.NewLine}{'\t'}""{nameof(DatabaseEntity.BooleanField)}""", + new[] { new SqlCommandParameter("param_0", 1, typeof(int)) }, + log)), + new IDatabaseEntity[] { databaseEntity } + }; + yield return new object[] + { + $"{nameof(DataAccess.Orm.Sql.Postgres)} - update entity with multiple sets", + new Func(container => container.Resolve().Update().Set(it => it.IntField.Assign(it.IntField + 1)).Set(it => it.Enum.Assign(EnEnum.Two)).Where(it => it.BooleanField == true).CachedExpression(Guid.NewGuid().ToString()).Invoke(token).Result), + new Action( + (query, log) => CheckSqlCommand(query, + $@"UPDATE ""{schema}"".""{nameof(DatabaseEntity)}""{Environment.NewLine}SET ""{nameof(DatabaseEntity.IntField)}"" = ""{nameof(DatabaseEntity.IntField)}"" + @param_0, ""{nameof(DatabaseEntity.Enum)}"" = @param_1{Environment.NewLine}WHERE{Environment.NewLine}{'\t'}""{nameof(DatabaseEntity.BooleanField)}"" = @param_2", + new[] { new SqlCommandParameter("param_0", 1, typeof(int)), new SqlCommandParameter("param_1", EnEnum.Two, typeof(EnEnum)), new SqlCommandParameter("param_2", true, typeof(bool)) }, + log)), + new IDatabaseEntity[] { databaseEntity } + }; + + /* + * Statistics + */ + + yield return new object[] + { + $"{nameof(DataAccess.Orm.Sql.Postgres)} - explain analyze", + new Func(container => container.Resolve().All().Select(it => new { it.Blog.Theme, Author = it.User.Nickname }).CachedExpression(Guid.NewGuid().ToString()).Explain(true, token).Result), + new Action( + (query, log) => CheckSqlCommand(query, + $@"EXPLAIN (ANALYZE, FORMAT json){Environment.NewLine}SELECT{Environment.NewLine}{'\t'}c.""{nameof(Post.Blog.Theme)}"",{Environment.NewLine}{'\t'}b.""{nameof(DatabaseEntities.Relations.User.Nickname)}"" AS ""Author""{Environment.NewLine}FROM{Environment.NewLine}{'\t'}""{schema}"".""{nameof(Blog)}"" c{Environment.NewLine}JOIN{Environment.NewLine}{'\t'}""{schema}"".""{nameof(DatabaseEntities.Relations.User)}"" b{Environment.NewLine}JOIN{Environment.NewLine}{'\t'}""{schema}"".""{nameof(Post)}"" a{Environment.NewLine}ON{Environment.NewLine}{'\t'}b.""{nameof(DatabaseEntities.Relations.User.PrimaryKey)}"" = a.""{nameof(Post.User)}""{Environment.NewLine}ON{Environment.NewLine}{'\t'}c.""{nameof(Blog.PrimaryKey)}"" = a.""{nameof(Post.Blog)}""", + Array.Empty(), + log)), + new IDatabaseEntity[] { user, blog, post } + }; + } + + [Fact] + internal void Lambda_parameter_name_increments_sequentally() + { + var ctx = new TranslationContext(); + + var producers = Enumerable + .Range(0, 1000) + .Select(_ => ctx.NextLambdaParameterName()) + .Reverse() + .ToArray(); + + Assert.Equal("a", producers[0]()); + Assert.Equal("b", producers[1]()); + Assert.Equal("c", producers[2]()); + Assert.Equal("d", producers[3]()); + + Assert.Equal("y", producers[24]()); + Assert.Equal("z", producers[25]()); + Assert.Equal("aa", producers[26]()); + Assert.Equal("ab", producers[27]()); + + Assert.Equal("zy", producers[700]()); + Assert.Equal("zz", producers[701]()); + Assert.Equal("aaa", producers[702]()); + Assert.Equal("aab", producers[703]()); + } + + [Theory(Timeout = 60_000)] + [MemberData(nameof(LinqToSqlTestData))] + internal async Task Orm_generates_valid_queries( + Lazy host, + CancellationTokenSource cts, + AsyncCountdownEvent asyncCountdownEvent, + string section, + Func queryProducer, + Action checkCommand, + IDatabaseEntity[] entities) + { + try + { + Output.WriteLine(section); + + var endpointDependencyContainer = host.Value.GetEndpointDependencyContainer(TestIdentity.Endpoint10); + + var sqlDatabaseSettings = endpointDependencyContainer + .Resolve>() + .Get(); + + Assert.Equal("LinqToSqlTest", sqlDatabaseSettings.Database); + Assert.Equal(IsolationLevel.ReadCommitted, sqlDatabaseSettings.IsolationLevel); + Assert.Equal(1u, sqlDatabaseSettings.ConnectionPoolSize); + + var ormSettings = endpointDependencyContainer + .Resolve>() + .Get(); + + Assert.Equal(10u, ormSettings.CommandSecondsTimeout); + + var hostShutdown = host.Value.WaitForShutdownAsync(cts.Token); + + var assert = Run(endpointDependencyContainer, queryProducer, checkCommand, entities, Output, cts.Token); + + var awaiter = Task.WhenAny(hostShutdown, assert); + + var result = await awaiter.ConfigureAwait(false); + + if (hostShutdown == result) + { + throw new InvalidOperationException("Host was unexpectedly stopped"); + } + + await result.ConfigureAwait(false); + } + finally + { + asyncCountdownEvent.Decrement(); + + if (asyncCountdownEvent.Read() == 0) + { + Output.WriteLine("CLEANUP"); + + try + { + await host + .Value + .StopAsync(cts.Token) + .ConfigureAwait(false); + } + finally + { + cts.Dispose(); + host.Value.Dispose(); + } + } + } + } + + private static Task Run( + IDependencyContainer dependencyContainer, + Func queryProducer, + Action checkCommand, + IDatabaseEntity[] entities, + ITestOutputHelper output, + CancellationToken token) + { + return dependencyContainer.InvokeWithinTransaction(false, RunWithinTransaction, token); + + async Task RunWithinTransaction(IAdvancedDatabaseTransaction transaction, CancellationToken token) + { + if (entities.Any()) + { + await transaction + .Insert(entities, EnInsertBehavior.Default) + .CachedExpression(Guid.NewGuid().ToString()) + .Invoke(token) + .ConfigureAwait(false); + } + + var collector = dependencyContainer.Resolve(); + + Expression expression; + object? item; + + var queryOrItem = queryProducer(dependencyContainer); + + if (queryOrItem is IQueryable queryable) + { + expression = queryable.Expression; + + item = queryable + .GetEnumerator() + .AsEnumerable() + .Single(); + } + else + { + expression = collector.Expressions.Last(); + + item = queryOrItem; + } + + var command = dependencyContainer + .Resolve() + .Translate(expression); + + checkCommand(command, output); + + Assert.NotNull(item); + + output.WriteLine("Dump:"); + + output.WriteLine(item is JsonValue jsonValue + ? jsonValue.ToString() + : item.Dump(BindingFlags.Instance | BindingFlags.Public | BindingFlags.GetProperty)); + } + } + + private static void CheckSqlCommand( + ICommand command, + string expectedQuery, + IReadOnlyCollection expectedQueryParameters, + ITestOutputHelper output) + { + var sqlCommand = (SqlCommand)command; + + output.WriteLine("Expected query:"); + output.WriteLine(expectedQuery); + + output.WriteLine("Actual query:"); + output.WriteLine(sqlCommand.CommandText); + + output.WriteLine("Expected parameters:"); + output.WriteLine(FormatParameters(expectedQueryParameters)); + + output.WriteLine("Actual parameters:"); + output.WriteLine(FormatParameters(sqlCommand.CommandParameters)); + + Assert.Equal(expectedQuery, sqlCommand.CommandText, StringComparer.Ordinal); + CheckParameters(output, sqlCommand, expectedQueryParameters); + } + + private static string FormatParameters(IReadOnlyCollection queryParameters) + { + return queryParameters.Any() + ? queryParameters.ToString(" ") + : "empty parameters"; + } + + private static void CheckParameters( + ITestOutputHelper output, + SqlCommand command, + IReadOnlyCollection expectedQueryParameters) + { + var parameters = command + .CommandParameters + .FullOuterJoin(expectedQueryParameters, + actual => actual.Name, + expected => expected.Name, + (actual, expected) => (actual, expected), + StringComparer.OrdinalIgnoreCase) + .ToList(); + + foreach (var (actual, expected) in parameters) + { + if (actual != null + && expected != null + && IsVersionParameter(output, command.CommandText, actual)) + { + Assert.NotEqual(0L, actual.Value); + Assert.Equal(expected.Type, actual.Type); + } + else + { + Assert.Equal(expected?.Value, actual?.Value); + Assert.Equal(expected?.Type, actual?.Type); + } + } + + static bool IsVersionParameter( + ITestOutputHelper output, + string commandText, + SqlCommandParameter parameter) + { + if (!commandText.Contains("INSERT INTO", StringComparison.OrdinalIgnoreCase)) + { + return false; + } + + output.WriteLine($"@{parameter.Name}"); + + var parameterIndex = commandText.IndexOf($"@{parameter.Name}", StringComparison.OrdinalIgnoreCase); + + HashSet commandParameters; + { + var left = commandText[..parameterIndex].LastIndexOf("(", StringComparison.OrdinalIgnoreCase); + var right = commandText[parameterIndex..].IndexOf(")", StringComparison.OrdinalIgnoreCase); + + commandParameters = commandText + .Substring(left + 1, parameterIndex - left + right - 1) + .Split(new[] { ",", " ", "\"", "(", ")" }, StringSplitOptions.RemoveEmptyEntries) + .ToHashSet(StringComparer.OrdinalIgnoreCase); + + output.WriteLine(commandText.Substring(left + 1, parameterIndex - left + right - 1)); + } + + var insertIndex = commandText[..parameterIndex].LastIndexOf("INSERT INTO", StringComparison.OrdinalIgnoreCase); + + HashSet columns; + { + var left = commandText[insertIndex..].IndexOf("(", StringComparison.OrdinalIgnoreCase); + var right = commandText[insertIndex..].IndexOf(")", StringComparison.OrdinalIgnoreCase); + + columns = commandText[insertIndex..] + .Substring(left + 1, right - left - 1) + .Split(new[] { ",", " ", "\"", "(", ")" }, StringSplitOptions.RemoveEmptyEntries) + .ToHashSet(StringComparer.OrdinalIgnoreCase); + + output.WriteLine(commandText.Substring(left + 1, right - left - 1)); + } + + Assert.Equal(commandParameters.Count, columns.Count); + Assert.Contains($"@{parameter.Name}", commandParameters); + + var columnName = commandParameters + .Zip(columns) + .ToDictionary( + pair => pair.First, + pair => pair.Second, + StringComparer.OrdinalIgnoreCase)[$"@{parameter.Name}"]; + + return columnName.Equals(nameof(IDatabaseEntity.Version), StringComparison.OrdinalIgnoreCase); + } + } + } +} \ No newline at end of file diff --git a/tests/Tests/GenericHost.Test/MessageHandlers/AlwaysReplyRequestHandler.cs b/tests/Tests/GenericHost.Test/MessageHandlers/AlwaysReplyRequestHandler.cs new file mode 100644 index 00000000..bc74ba25 --- /dev/null +++ b/tests/Tests/GenericHost.Test/MessageHandlers/AlwaysReplyRequestHandler.cs @@ -0,0 +1,27 @@ +namespace SpaceEngineers.Core.GenericHost.Test.MessageHandlers +{ + using System.Threading; + using System.Threading.Tasks; + using AutoRegistration.Api.Abstractions; + using AutoRegistration.Api.Attributes; + using AutoRegistration.Api.Enumerations; + using GenericEndpoint.Api.Abstractions; + using Messages; + + [Component(EnLifestyle.Transient)] + internal class AlwaysReplyRequestHandler : IMessageHandler, + IResolvable> + { + private readonly IIntegrationContext _context; + + public AlwaysReplyRequestHandler(IIntegrationContext context) + { + _context = context; + } + + public Task Handle(Request message, CancellationToken token) + { + return _context.Reply(message, new Reply(message.Id), token); + } + } +} \ No newline at end of file diff --git a/tests/Tests/GenericHost.Test/MessageHandlers/BaseEventHandler.cs b/tests/Tests/GenericHost.Test/MessageHandlers/BaseEventHandler.cs new file mode 100644 index 00000000..a6d3dd13 --- /dev/null +++ b/tests/Tests/GenericHost.Test/MessageHandlers/BaseEventHandler.cs @@ -0,0 +1,30 @@ +namespace SpaceEngineers.Core.GenericHost.Test.MessageHandlers +{ + using System.Threading; + using System.Threading.Tasks; + using AutoRegistration.Api.Abstractions; + using AutoRegistration.Api.Attributes; + using AutoRegistration.Api.Enumerations; + using GenericEndpoint.Api.Abstractions; + using GenericEndpoint.Contract; + using Messages; + + [Component(EnLifestyle.Transient)] + internal class BaseEventHandler : IMessageHandler, + IResolvable> + { + private readonly EndpointIdentity _endpointIdentity; + private readonly IIntegrationContext _context; + + public BaseEventHandler(EndpointIdentity endpointIdentity, IIntegrationContext context) + { + _endpointIdentity = endpointIdentity; + _context = context; + } + + public Task Handle(BaseEvent message, CancellationToken token) + { + return _context.Publish(new HandlerInvoked(typeof(BaseEventHandler), _endpointIdentity), token); + } + } +} \ No newline at end of file diff --git a/tests/Tests/GenericHost.Test/MessageHandlers/CommandHandler.cs b/tests/Tests/GenericHost.Test/MessageHandlers/CommandHandler.cs new file mode 100644 index 00000000..234d2244 --- /dev/null +++ b/tests/Tests/GenericHost.Test/MessageHandlers/CommandHandler.cs @@ -0,0 +1,30 @@ +namespace SpaceEngineers.Core.GenericHost.Test.MessageHandlers +{ + using System.Threading; + using System.Threading.Tasks; + using AutoRegistration.Api.Abstractions; + using AutoRegistration.Api.Attributes; + using AutoRegistration.Api.Enumerations; + using GenericEndpoint.Api.Abstractions; + using GenericEndpoint.Contract; + using Messages; + + [Component(EnLifestyle.Transient)] + internal class CommandHandler : IMessageHandler, + IResolvable> + { + private readonly EndpointIdentity _endpointIdentity; + private readonly IIntegrationContext _context; + + public CommandHandler(EndpointIdentity endpointIdentity, IIntegrationContext context) + { + _endpointIdentity = endpointIdentity; + _context = context; + } + + public Task Handle(Command message, CancellationToken token) + { + return _context.Publish(new HandlerInvoked(typeof(CommandHandler), _endpointIdentity), token); + } + } +} \ No newline at end of file diff --git a/tests/Tests/GenericHost.Test/MessageHandlers/EventHandler.cs b/tests/Tests/GenericHost.Test/MessageHandlers/EventHandler.cs new file mode 100644 index 00000000..cd9b82aa --- /dev/null +++ b/tests/Tests/GenericHost.Test/MessageHandlers/EventHandler.cs @@ -0,0 +1,30 @@ +namespace SpaceEngineers.Core.GenericHost.Test.MessageHandlers +{ + using System.Threading; + using System.Threading.Tasks; + using AutoRegistration.Api.Abstractions; + using AutoRegistration.Api.Attributes; + using AutoRegistration.Api.Enumerations; + using GenericEndpoint.Api.Abstractions; + using GenericEndpoint.Contract; + using Messages; + + [Component(EnLifestyle.Transient)] + internal class EventHandler : IMessageHandler, + IResolvable> + { + private readonly EndpointIdentity _endpointIdentity; + private readonly IIntegrationContext _context; + + public EventHandler(EndpointIdentity endpointIdentity, IIntegrationContext context) + { + _endpointIdentity = endpointIdentity; + _context = context; + } + + public Task Handle(Event message, CancellationToken token) + { + return _context.Publish(new HandlerInvoked(typeof(EventHandler), _endpointIdentity), token); + } + } +} \ No newline at end of file diff --git a/tests/Tests/GenericHost.Test/MessageHandlers/InheritedEventHandler.cs b/tests/Tests/GenericHost.Test/MessageHandlers/InheritedEventHandler.cs new file mode 100644 index 00000000..978d1246 --- /dev/null +++ b/tests/Tests/GenericHost.Test/MessageHandlers/InheritedEventHandler.cs @@ -0,0 +1,30 @@ +namespace SpaceEngineers.Core.GenericHost.Test.MessageHandlers +{ + using System.Threading; + using System.Threading.Tasks; + using AutoRegistration.Api.Abstractions; + using AutoRegistration.Api.Attributes; + using AutoRegistration.Api.Enumerations; + using GenericEndpoint.Api.Abstractions; + using GenericEndpoint.Contract; + using Messages; + + [Component(EnLifestyle.Transient)] + internal class InheritedEventHandler : IMessageHandler, + IResolvable> + { + private readonly EndpointIdentity _endpointIdentity; + private readonly IIntegrationContext _context; + + public InheritedEventHandler(EndpointIdentity endpointIdentity, IIntegrationContext context) + { + _endpointIdentity = endpointIdentity; + _context = context; + } + + public Task Handle(InheritedEvent message, CancellationToken token) + { + return _context.Publish(new HandlerInvoked(typeof(InheritedEventHandler), _endpointIdentity), token); + } + } +} \ No newline at end of file diff --git a/tests/Tests/GenericHost.Test/MessageHandlers/IntroduceDatabaseChangesCommandHandler.cs b/tests/Tests/GenericHost.Test/MessageHandlers/IntroduceDatabaseChangesCommandHandler.cs new file mode 100644 index 00000000..3bd59975 --- /dev/null +++ b/tests/Tests/GenericHost.Test/MessageHandlers/IntroduceDatabaseChangesCommandHandler.cs @@ -0,0 +1,33 @@ +namespace SpaceEngineers.Core.GenericHost.Test.MessageHandlers +{ + using System.Threading; + using System.Threading.Tasks; + using AutoRegistration.Api.Abstractions; + using AutoRegistration.Api.Attributes; + using AutoRegistration.Api.Enumerations; + using DataAccess.Orm.Sql.Linq; + using DataAccess.Orm.Sql.Transaction; + using DatabaseEntities; + using GenericEndpoint.Api.Abstractions; + using Messages; + + [Component(EnLifestyle.Transient)] + internal class IntroduceDatabaseChangesCommandHandler : IMessageHandler, + IResolvable> + { + private readonly IDatabaseContext _databaseContext; + + public IntroduceDatabaseChangesCommandHandler(IDatabaseContext databaseContext) + { + _databaseContext = databaseContext; + } + + public Task Handle(Command message, CancellationToken token) + { + return _databaseContext + .Insert(new[] { DatabaseEntity.Generate() }, EnInsertBehavior.Default) + .CachedExpression("8981C2BE-2FE9-4932-841B-94A31C3DE136") + .Invoke(token); + } + } +} \ No newline at end of file diff --git a/tests/Tests/GenericHost.Test/MessageHandlers/IntroduceDatabaseChangesEventHandler.cs b/tests/Tests/GenericHost.Test/MessageHandlers/IntroduceDatabaseChangesEventHandler.cs new file mode 100644 index 00000000..2d8573ab --- /dev/null +++ b/tests/Tests/GenericHost.Test/MessageHandlers/IntroduceDatabaseChangesEventHandler.cs @@ -0,0 +1,33 @@ +namespace SpaceEngineers.Core.GenericHost.Test.MessageHandlers +{ + using System.Threading; + using System.Threading.Tasks; + using AutoRegistration.Api.Abstractions; + using AutoRegistration.Api.Attributes; + using AutoRegistration.Api.Enumerations; + using DataAccess.Orm.Sql.Linq; + using DataAccess.Orm.Sql.Transaction; + using DatabaseEntities; + using GenericEndpoint.Api.Abstractions; + using Messages; + + [Component(EnLifestyle.Transient)] + internal class IntroduceDatabaseChangesEventHandler : IMessageHandler, + IResolvable> + { + private readonly IDatabaseContext _databaseContext; + + public IntroduceDatabaseChangesEventHandler(IDatabaseContext databaseContext) + { + _databaseContext = databaseContext; + } + + public Task Handle(Event message, CancellationToken token) + { + return _databaseContext + .Insert(new[] { DatabaseEntity.Generate() }, EnInsertBehavior.Default) + .CachedExpression("5D1CD5BF-9BBA-4CCD-83F4-99A60BCEDACB") + .Invoke(token); + } + } +} \ No newline at end of file diff --git a/tests/Tests/GenericHost.Test/MessageHandlers/IntroduceDatabaseChangesReplyHandler.cs b/tests/Tests/GenericHost.Test/MessageHandlers/IntroduceDatabaseChangesReplyHandler.cs new file mode 100644 index 00000000..72fb9649 --- /dev/null +++ b/tests/Tests/GenericHost.Test/MessageHandlers/IntroduceDatabaseChangesReplyHandler.cs @@ -0,0 +1,33 @@ +namespace SpaceEngineers.Core.GenericHost.Test.MessageHandlers +{ + using System.Threading; + using System.Threading.Tasks; + using AutoRegistration.Api.Abstractions; + using AutoRegistration.Api.Attributes; + using AutoRegistration.Api.Enumerations; + using DataAccess.Orm.Sql.Linq; + using DataAccess.Orm.Sql.Transaction; + using DatabaseEntities; + using GenericEndpoint.Api.Abstractions; + using Messages; + + [Component(EnLifestyle.Transient)] + internal class IntroduceDatabaseChangesReplyHandler : IMessageHandler, + IResolvable> + { + private readonly IDatabaseContext _databaseContext; + + public IntroduceDatabaseChangesReplyHandler(IDatabaseContext databaseContext) + { + _databaseContext = databaseContext; + } + + public Task Handle(Reply message, CancellationToken token) + { + return _databaseContext + .Insert(new[] { DatabaseEntity.Generate() }, EnInsertBehavior.Default) + .CachedExpression("63A280FE-CCCD-4C13-B357-4DD40EA345A6") + .Invoke(token); + } + } +} \ No newline at end of file diff --git a/tests/Tests/GenericHost.Test/MessageHandlers/IntroduceDatabaseChangesRequestHandler.cs b/tests/Tests/GenericHost.Test/MessageHandlers/IntroduceDatabaseChangesRequestHandler.cs new file mode 100644 index 00000000..5334f9b0 --- /dev/null +++ b/tests/Tests/GenericHost.Test/MessageHandlers/IntroduceDatabaseChangesRequestHandler.cs @@ -0,0 +1,42 @@ +namespace SpaceEngineers.Core.GenericHost.Test.MessageHandlers +{ + using System.Threading; + using System.Threading.Tasks; + using AutoRegistration.Api.Abstractions; + using AutoRegistration.Api.Attributes; + using AutoRegistration.Api.Enumerations; + using DataAccess.Orm.Sql.Linq; + using DataAccess.Orm.Sql.Transaction; + using DatabaseEntities; + using GenericEndpoint.Api.Abstractions; + using Messages; + + [Component(EnLifestyle.Transient)] + internal class IntroduceDatabaseChangesRequestHandler : IMessageHandler, + IResolvable> + { + private readonly IIntegrationContext _context; + private readonly IDatabaseContext _databaseContext; + + public IntroduceDatabaseChangesRequestHandler( + IIntegrationContext context, + IDatabaseContext databaseContext) + { + _context = context; + _databaseContext = databaseContext; + } + + public async Task Handle(Request message, CancellationToken token) + { + await _databaseContext + .Insert(new[] { DatabaseEntity.Generate() }, EnInsertBehavior.Default) + .CachedExpression("BD9DABE3-B49F-4D3E-ADCC-3C22B387AA14") + .Invoke(token) + .ConfigureAwait(false); + + await _context + .Reply(message, new Reply(message.Id), token) + .ConfigureAwait(false); + } + } +} \ No newline at end of file diff --git a/tests/Tests/GenericHost.Test/MessageHandlers/MakeRequestCommandHandler.cs b/tests/Tests/GenericHost.Test/MessageHandlers/MakeRequestCommandHandler.cs new file mode 100644 index 00000000..4e47943a --- /dev/null +++ b/tests/Tests/GenericHost.Test/MessageHandlers/MakeRequestCommandHandler.cs @@ -0,0 +1,29 @@ +namespace SpaceEngineers.Core.GenericHost.Test.MessageHandlers +{ + using System.Threading; + using System.Threading.Tasks; + using AutoRegistration.Api.Abstractions; + using AutoRegistration.Api.Attributes; + using AutoRegistration.Api.Enumerations; + using GenericEndpoint.Api.Abstractions; + using Messages; + + [Component(EnLifestyle.Transient)] + internal class MakeRequestCommandHandler : IMessageHandler, + IResolvable> + { + private readonly IIntegrationContext _context; + + public MakeRequestCommandHandler(IIntegrationContext context) + { + _context = context; + } + + public Task Handle(MakeRequestCommand message, CancellationToken token) + { + var request = new Request(message.Id); + + return _context.Request(request, token); + } + } +} \ No newline at end of file diff --git a/tests/Tests/GenericHost.Test/MessageHandlers/MakeRpcRequestCommandHandler.cs b/tests/Tests/GenericHost.Test/MessageHandlers/MakeRpcRequestCommandHandler.cs new file mode 100644 index 00000000..48b423f7 --- /dev/null +++ b/tests/Tests/GenericHost.Test/MessageHandlers/MakeRpcRequestCommandHandler.cs @@ -0,0 +1,38 @@ +namespace SpaceEngineers.Core.GenericHost.Test.MessageHandlers +{ + using System.Threading; + using System.Threading.Tasks; + using AutoRegistration.Api.Abstractions; + using AutoRegistration.Api.Attributes; + using AutoRegistration.Api.Enumerations; + using GenericEndpoint.Api.Abstractions; + using GenericEndpoint.Contract; + using Messages; + + [Component(EnLifestyle.Transient)] + internal class MakeRpcRequestCommandHandler : IMessageHandler, + IResolvable> + { + private readonly EndpointIdentity _endpointIdentity; + private readonly IIntegrationContext _context; + + public MakeRpcRequestCommandHandler(EndpointIdentity endpointIdentity, IIntegrationContext context) + { + _endpointIdentity = endpointIdentity; + _context = context; + } + + public async Task Handle(MakeRpcRequestCommand message, CancellationToken token) + { + var request = new Request(message.Id); + + var reply = await _context + .RpcRequest(request, token) + .ConfigureAwait(false); + + await _context + .Publish(new HandlerInvoked(typeof(MakeRpcRequestCommandHandler), _endpointIdentity), token) + .ConfigureAwait(false); + } + } +} \ No newline at end of file diff --git a/tests/Tests/GenericHost.Test/MessageHandlers/OddReplyRequestHandler.cs b/tests/Tests/GenericHost.Test/MessageHandlers/OddReplyRequestHandler.cs new file mode 100644 index 00000000..42fac44b --- /dev/null +++ b/tests/Tests/GenericHost.Test/MessageHandlers/OddReplyRequestHandler.cs @@ -0,0 +1,29 @@ +namespace SpaceEngineers.Core.GenericHost.Test.MessageHandlers +{ + using System.Threading; + using System.Threading.Tasks; + using AutoRegistration.Api.Abstractions; + using AutoRegistration.Api.Attributes; + using AutoRegistration.Api.Enumerations; + using GenericEndpoint.Api.Abstractions; + using Messages; + + [Component(EnLifestyle.Transient)] + internal class OddReplyRequestHandler : IMessageHandler, + IResolvable> + { + private readonly IIntegrationContext _context; + + public OddReplyRequestHandler(IIntegrationContext context) + { + _context = context; + } + + public Task Handle(Request message, CancellationToken token) + { + return message.Id % 2 == 0 + ? Task.CompletedTask + : _context.Reply(message, new Reply(message.Id), token); + } + } +} \ No newline at end of file diff --git a/tests/Tests/GenericHost.Test/MessageHandlers/OpenGenericCommandHandler.cs b/tests/Tests/GenericHost.Test/MessageHandlers/OpenGenericCommandHandler.cs new file mode 100644 index 00000000..0490a327 --- /dev/null +++ b/tests/Tests/GenericHost.Test/MessageHandlers/OpenGenericCommandHandler.cs @@ -0,0 +1,31 @@ +namespace SpaceEngineers.Core.GenericHost.Test.MessageHandlers +{ + using System.Threading; + using System.Threading.Tasks; + using AutoRegistration.Api.Abstractions; + using AutoRegistration.Api.Attributes; + using AutoRegistration.Api.Enumerations; + using GenericEndpoint.Api.Abstractions; + using GenericEndpoint.Contract; + using Messages; + + [Component(EnLifestyle.Transient)] + internal class OpenGenericCommandHandler : IMessageHandler, + IResolvable> + where TCommand : OpenGenericHandlerCommand + { + private readonly EndpointIdentity _endpointIdentity; + private readonly IIntegrationContext _context; + + public OpenGenericCommandHandler(EndpointIdentity endpointIdentity, IIntegrationContext context) + { + _endpointIdentity = endpointIdentity; + _context = context; + } + + public Task Handle(TCommand message, CancellationToken token) + { + return _context.Publish(new HandlerInvoked(typeof(OpenGenericCommandHandler), _endpointIdentity), token); + } + } +} \ No newline at end of file diff --git a/tests/Tests/GenericHost.Test/MessageHandlers/PublishEventCommandHandler.cs b/tests/Tests/GenericHost.Test/MessageHandlers/PublishEventCommandHandler.cs new file mode 100644 index 00000000..adad7e59 --- /dev/null +++ b/tests/Tests/GenericHost.Test/MessageHandlers/PublishEventCommandHandler.cs @@ -0,0 +1,27 @@ +namespace SpaceEngineers.Core.GenericHost.Test.MessageHandlers +{ + using System.Threading; + using System.Threading.Tasks; + using AutoRegistration.Api.Abstractions; + using AutoRegistration.Api.Attributes; + using AutoRegistration.Api.Enumerations; + using GenericEndpoint.Api.Abstractions; + using Messages; + + [Component(EnLifestyle.Transient)] + internal class PublishEventCommandHandler : IMessageHandler, + IResolvable> + { + private readonly IIntegrationContext _context; + + public PublishEventCommandHandler(IIntegrationContext context) + { + _context = context; + } + + public Task Handle(PublishEventCommand message, CancellationToken token) + { + return _context.Publish(new Event(message.Id), token); + } + } +} \ No newline at end of file diff --git a/tests/Tests/GenericHost.Test/MessageHandlers/PublishInheritedEventCommandHandler.cs b/tests/Tests/GenericHost.Test/MessageHandlers/PublishInheritedEventCommandHandler.cs new file mode 100644 index 00000000..8481b08b --- /dev/null +++ b/tests/Tests/GenericHost.Test/MessageHandlers/PublishInheritedEventCommandHandler.cs @@ -0,0 +1,27 @@ +namespace SpaceEngineers.Core.GenericHost.Test.MessageHandlers +{ + using System.Threading; + using System.Threading.Tasks; + using AutoRegistration.Api.Abstractions; + using AutoRegistration.Api.Attributes; + using AutoRegistration.Api.Enumerations; + using GenericEndpoint.Api.Abstractions; + using Messages; + + [Component(EnLifestyle.Transient)] + internal class PublishInheritedEventCommandHandler : IMessageHandler, + IResolvable> + { + private readonly IIntegrationContext _context; + + public PublishInheritedEventCommandHandler(IIntegrationContext context) + { + _context = context; + } + + public Task Handle(PublishInheritedEventCommand message, CancellationToken token) + { + return _context.Publish(new InheritedEvent(message.Id), token); + } + } +} \ No newline at end of file diff --git a/tests/Tests/GenericHost.Test/MessageHandlers/ReplyHandler.cs b/tests/Tests/GenericHost.Test/MessageHandlers/ReplyHandler.cs new file mode 100644 index 00000000..9d982cca --- /dev/null +++ b/tests/Tests/GenericHost.Test/MessageHandlers/ReplyHandler.cs @@ -0,0 +1,30 @@ +namespace SpaceEngineers.Core.GenericHost.Test.MessageHandlers +{ + using System.Threading; + using System.Threading.Tasks; + using AutoRegistration.Api.Abstractions; + using AutoRegistration.Api.Attributes; + using AutoRegistration.Api.Enumerations; + using GenericEndpoint.Api.Abstractions; + using GenericEndpoint.Contract; + using Messages; + + [Component(EnLifestyle.Transient)] + internal class ReplyHandler : IMessageHandler, + IResolvable> + { + private readonly EndpointIdentity _endpointIdentity; + private readonly IIntegrationContext _context; + + public ReplyHandler(EndpointIdentity endpointIdentity, IIntegrationContext context) + { + _endpointIdentity = endpointIdentity; + _context = context; + } + + public Task Handle(Reply message, CancellationToken token) + { + return _context.Publish(new HandlerInvoked(typeof(ReplyHandler), _endpointIdentity), token); + } + } +} \ No newline at end of file diff --git a/tests/Tests/GenericHost.Test/MessageHandlers/SendDelayedCommandEventHandler.cs b/tests/Tests/GenericHost.Test/MessageHandlers/SendDelayedCommandEventHandler.cs new file mode 100644 index 00000000..9dd55f24 --- /dev/null +++ b/tests/Tests/GenericHost.Test/MessageHandlers/SendDelayedCommandEventHandler.cs @@ -0,0 +1,28 @@ +namespace SpaceEngineers.Core.GenericHost.Test.MessageHandlers +{ + using System; + using System.Threading; + using System.Threading.Tasks; + using AutoRegistration.Api.Abstractions; + using AutoRegistration.Api.Attributes; + using AutoRegistration.Api.Enumerations; + using GenericEndpoint.Api.Abstractions; + using Messages; + + [Component(EnLifestyle.Transient)] + internal class SendDelayedCommandEventHandler : IMessageHandler, + IResolvable> + { + private readonly IIntegrationContext _context; + + public SendDelayedCommandEventHandler(IIntegrationContext context) + { + _context = context; + } + + public Task Handle(Event message, CancellationToken token) + { + return _context.Delay(new Command(message.Id), DateTime.UtcNow + TimeSpan.FromDays(message.Id), token); + } + } +} \ No newline at end of file diff --git a/tests/Tests/GenericHost.Test/MessageHandlers/ThrowingCommandHandler.cs b/tests/Tests/GenericHost.Test/MessageHandlers/ThrowingCommandHandler.cs new file mode 100644 index 00000000..a2d002c8 --- /dev/null +++ b/tests/Tests/GenericHost.Test/MessageHandlers/ThrowingCommandHandler.cs @@ -0,0 +1,22 @@ +namespace SpaceEngineers.Core.GenericHost.Test.MessageHandlers +{ + using System; + using System.Globalization; + using System.Threading; + using System.Threading.Tasks; + using AutoRegistration.Api.Abstractions; + using AutoRegistration.Api.Attributes; + using AutoRegistration.Api.Enumerations; + using GenericEndpoint.Api.Abstractions; + using Messages; + + [Component(EnLifestyle.Transient)] + internal class ThrowingCommandHandler : IMessageHandler, + IResolvable> + { + public Task Handle(Command message, CancellationToken token) + { + throw new InvalidOperationException(message.Id.ToString(CultureInfo.InvariantCulture)); + } + } +} \ No newline at end of file diff --git a/tests/Tests/GenericHost.Test/Messages/BaseEvent.cs b/tests/Tests/GenericHost.Test/Messages/BaseEvent.cs new file mode 100644 index 00000000..9e30786f --- /dev/null +++ b/tests/Tests/GenericHost.Test/Messages/BaseEvent.cs @@ -0,0 +1,23 @@ +namespace SpaceEngineers.Core.GenericHost.Test.Messages +{ + using System.Globalization; + using GenericEndpoint.Contract.Abstractions; + using GenericEndpoint.Contract.Attributes; + + [OwnedBy(TestIdentity.Endpoint1)] + [Feature(TestFeatures.Test)] + internal record BaseEvent : IIntegrationEvent + { + public BaseEvent(int id) + { + Id = id; + } + + public int Id { get; init; } + + public override string ToString() + { + return Id.ToString(CultureInfo.InvariantCulture); + } + } +} \ No newline at end of file diff --git a/tests/Tests/GenericHost.Test/Messages/Command.cs b/tests/Tests/GenericHost.Test/Messages/Command.cs new file mode 100644 index 00000000..855aee78 --- /dev/null +++ b/tests/Tests/GenericHost.Test/Messages/Command.cs @@ -0,0 +1,23 @@ +namespace SpaceEngineers.Core.GenericHost.Test.Messages +{ + using System.Globalization; + using GenericEndpoint.Contract.Abstractions; + using GenericEndpoint.Contract.Attributes; + + [OwnedBy(TestIdentity.Endpoint1)] + [Feature(TestFeatures.Test)] + internal record Command : IIntegrationCommand + { + public Command(int id) + { + Id = id; + } + + public int Id { get; init; } + + public override string ToString() + { + return Id.ToString(CultureInfo.InvariantCulture); + } + } +} \ No newline at end of file diff --git a/tests/Tests/GenericHost.Test/Messages/Event.cs b/tests/Tests/GenericHost.Test/Messages/Event.cs new file mode 100644 index 00000000..97c4bcce --- /dev/null +++ b/tests/Tests/GenericHost.Test/Messages/Event.cs @@ -0,0 +1,23 @@ +namespace SpaceEngineers.Core.GenericHost.Test.Messages +{ + using System.Globalization; + using GenericEndpoint.Contract.Abstractions; + using GenericEndpoint.Contract.Attributes; + + [OwnedBy(TestIdentity.Endpoint2)] + [Feature(TestFeatures.Test)] + internal record Event : IIntegrationEvent + { + public Event(int id) + { + Id = id; + } + + public int Id { get; init; } + + public override string ToString() + { + return Id.ToString(CultureInfo.InvariantCulture); + } + } +} \ No newline at end of file diff --git a/tests/Tests/GenericHost.Test/Messages/HandlerInvoked.cs b/tests/Tests/GenericHost.Test/Messages/HandlerInvoked.cs new file mode 100644 index 00000000..107cfcb6 --- /dev/null +++ b/tests/Tests/GenericHost.Test/Messages/HandlerInvoked.cs @@ -0,0 +1,31 @@ +namespace SpaceEngineers.Core.GenericHost.Test.Messages +{ + using System; + using System.Text.Json.Serialization; + using GenericEndpoint.Contract; + using GenericEndpoint.Contract.Abstractions; + using GenericEndpoint.Contract.Attributes; + + [OwnedBy(nameof(EndpointIdentity))] + [Feature(TestFeatures.Test)] + internal record HandlerInvoked : IIntegrationEvent + { + [JsonConstructor] + [Obsolete("serialization constructor")] + public HandlerInvoked() + { + HandlerType = default!; + EndpointIdentity = default!; + } + + public HandlerInvoked(Type handlerType, EndpointIdentity endpointIdentity) + { + HandlerType = handlerType; + EndpointIdentity = endpointIdentity; + } + + public Type HandlerType { get; init; } + + public EndpointIdentity EndpointIdentity { get; init; } + } +} \ No newline at end of file diff --git a/tests/Tests/GenericHost.Test/Messages/InheritedEvent.cs b/tests/Tests/GenericHost.Test/Messages/InheritedEvent.cs new file mode 100644 index 00000000..0ce68e7c --- /dev/null +++ b/tests/Tests/GenericHost.Test/Messages/InheritedEvent.cs @@ -0,0 +1,14 @@ +namespace SpaceEngineers.Core.GenericHost.Test.Messages +{ + using GenericEndpoint.Contract.Attributes; + + [OwnedBy(TestIdentity.Endpoint1)] + [Feature(TestFeatures.Test)] + internal record InheritedEvent : BaseEvent + { + public InheritedEvent(int id) + : base(id) + { + } + } +} \ No newline at end of file diff --git a/tests/Tests/GenericHost.Test/Messages/MakeRequestCommand.cs b/tests/Tests/GenericHost.Test/Messages/MakeRequestCommand.cs new file mode 100644 index 00000000..09bcb2eb --- /dev/null +++ b/tests/Tests/GenericHost.Test/Messages/MakeRequestCommand.cs @@ -0,0 +1,23 @@ +namespace SpaceEngineers.Core.GenericHost.Test.Messages +{ + using System.Globalization; + using GenericEndpoint.Contract.Abstractions; + using GenericEndpoint.Contract.Attributes; + + [OwnedBy(TestIdentity.Endpoint1)] + [Feature(TestFeatures.Test)] + internal record MakeRequestCommand : IIntegrationCommand + { + public MakeRequestCommand(int id) + { + Id = id; + } + + public int Id { get; init; } + + public override string ToString() + { + return Id.ToString(CultureInfo.InvariantCulture); + } + } +} \ No newline at end of file diff --git a/tests/Tests/GenericHost.Test/Messages/MakeRpcRequestCommand.cs b/tests/Tests/GenericHost.Test/Messages/MakeRpcRequestCommand.cs new file mode 100644 index 00000000..834ec9ec --- /dev/null +++ b/tests/Tests/GenericHost.Test/Messages/MakeRpcRequestCommand.cs @@ -0,0 +1,23 @@ +namespace SpaceEngineers.Core.GenericHost.Test.Messages +{ + using System.Globalization; + using GenericEndpoint.Contract.Abstractions; + using GenericEndpoint.Contract.Attributes; + + [OwnedBy(TestIdentity.Endpoint1)] + [Feature(TestFeatures.Test)] + internal record MakeRpcRequestCommand : IIntegrationCommand + { + public MakeRpcRequestCommand(int id) + { + Id = id; + } + + public int Id { get; init; } + + public override string ToString() + { + return Id.ToString(CultureInfo.InvariantCulture); + } + } +} \ No newline at end of file diff --git a/tests/Tests/GenericHost.Test/Messages/OpenGenericHandlerCommand.cs b/tests/Tests/GenericHost.Test/Messages/OpenGenericHandlerCommand.cs new file mode 100644 index 00000000..e4869306 --- /dev/null +++ b/tests/Tests/GenericHost.Test/Messages/OpenGenericHandlerCommand.cs @@ -0,0 +1,11 @@ +namespace SpaceEngineers.Core.GenericHost.Test.Messages +{ + using GenericEndpoint.Contract.Abstractions; + using GenericEndpoint.Contract.Attributes; + + [OwnedBy(TestIdentity.Endpoint1)] + [Feature(TestFeatures.Test)] + internal record OpenGenericHandlerCommand : IIntegrationCommand + { + } +} \ No newline at end of file diff --git a/tests/Tests/GenericHost.Test/Messages/PublishEventCommand.cs b/tests/Tests/GenericHost.Test/Messages/PublishEventCommand.cs new file mode 100644 index 00000000..d5706ff8 --- /dev/null +++ b/tests/Tests/GenericHost.Test/Messages/PublishEventCommand.cs @@ -0,0 +1,23 @@ +namespace SpaceEngineers.Core.GenericHost.Test.Messages +{ + using System.Globalization; + using GenericEndpoint.Contract.Abstractions; + using GenericEndpoint.Contract.Attributes; + + [OwnedBy(TestIdentity.Endpoint2)] + [Feature(TestFeatures.Test)] + internal record PublishEventCommand : IIntegrationCommand + { + public PublishEventCommand(int id) + { + Id = id; + } + + public int Id { get; init; } + + public override string ToString() + { + return Id.ToString(CultureInfo.InvariantCulture); + } + } +} \ No newline at end of file diff --git a/tests/Tests/GenericHost.Test/Messages/PublishInheritedEventCommand.cs b/tests/Tests/GenericHost.Test/Messages/PublishInheritedEventCommand.cs new file mode 100644 index 00000000..207d9af0 --- /dev/null +++ b/tests/Tests/GenericHost.Test/Messages/PublishInheritedEventCommand.cs @@ -0,0 +1,23 @@ +namespace SpaceEngineers.Core.GenericHost.Test.Messages +{ + using System.Globalization; + using GenericEndpoint.Contract.Abstractions; + using GenericEndpoint.Contract.Attributes; + + [OwnedBy(TestIdentity.Endpoint1)] + [Feature(TestFeatures.Test)] + internal record PublishInheritedEventCommand : IIntegrationCommand + { + public PublishInheritedEventCommand(int id) + { + Id = id; + } + + public int Id { get; init; } + + public override string ToString() + { + return Id.ToString(CultureInfo.InvariantCulture); + } + } +} \ No newline at end of file diff --git a/tests/Tests/GenericHost.Test/Messages/Reply.cs b/tests/Tests/GenericHost.Test/Messages/Reply.cs new file mode 100644 index 00000000..9ba809a9 --- /dev/null +++ b/tests/Tests/GenericHost.Test/Messages/Reply.cs @@ -0,0 +1,22 @@ +namespace SpaceEngineers.Core.GenericHost.Test.Messages +{ + using System.Globalization; + using GenericEndpoint.Contract.Abstractions; + using GenericEndpoint.Contract.Attributes; + + [Feature(TestFeatures.Test)] + internal record Reply : IIntegrationReply + { + public Reply(int id) + { + Id = id; + } + + public int Id { get; init; } + + public override string ToString() + { + return Id.ToString(CultureInfo.InvariantCulture); + } + } +} \ No newline at end of file diff --git a/tests/Tests/GenericHost.Test/Messages/Request.cs b/tests/Tests/GenericHost.Test/Messages/Request.cs new file mode 100644 index 00000000..aee220e5 --- /dev/null +++ b/tests/Tests/GenericHost.Test/Messages/Request.cs @@ -0,0 +1,23 @@ +namespace SpaceEngineers.Core.GenericHost.Test.Messages +{ + using System.Globalization; + using GenericEndpoint.Contract.Abstractions; + using GenericEndpoint.Contract.Attributes; + + [OwnedBy(TestIdentity.Endpoint1)] + [Feature(TestFeatures.Test)] + internal record Request : IIntegrationRequest + { + public Request(int id) + { + Id = id; + } + + public int Id { get; init; } + + public override string ToString() + { + return Id.ToString(CultureInfo.InvariantCulture); + } + } +} \ No newline at end of file diff --git a/tests/Tests/GenericHost.Test/Messages/TestFeatures.cs b/tests/Tests/GenericHost.Test/Messages/TestFeatures.cs new file mode 100644 index 00000000..3b0ca189 --- /dev/null +++ b/tests/Tests/GenericHost.Test/Messages/TestFeatures.cs @@ -0,0 +1,7 @@ +namespace SpaceEngineers.Core.GenericHost.Test.Messages +{ + internal static class TestFeatures + { + public const string Test = nameof(Test); + } +} \ No newline at end of file diff --git a/tests/Tests/GenericHost.Test/Messages/TestIdentity.cs b/tests/Tests/GenericHost.Test/Messages/TestIdentity.cs new file mode 100644 index 00000000..40f7510a --- /dev/null +++ b/tests/Tests/GenericHost.Test/Messages/TestIdentity.cs @@ -0,0 +1,21 @@ +namespace SpaceEngineers.Core.GenericHost.Test.Messages +{ + using System; + using System.Reflection; + using GenericEndpoint.Contract; + + internal static class TestIdentity + { + public const string Endpoint1 = nameof(Endpoint1); + + public const string Endpoint2 = nameof(Endpoint2); + + public static Assembly Endpoint1Assembly { get; } = Assembly.GetEntryAssembly() ?? throw new InvalidOperationException("Unable to get entry assembly"); + + public static Assembly Endpoint2Assembly { get; } = Assembly.GetEntryAssembly() ?? throw new InvalidOperationException("Unable to get entry assembly"); + + public static EndpointIdentity Endpoint10 { get; } = new EndpointIdentity(Endpoint1, Endpoint1Assembly); + + public static EndpointIdentity Endpoint20 { get; } = new EndpointIdentity(Endpoint2, Endpoint2Assembly); + } +} \ No newline at end of file diff --git a/tests/Tests/GenericHost.Test/Mocks/BackgroundOutboxDelivery.cs b/tests/Tests/GenericHost.Test/Mocks/BackgroundOutboxDelivery.cs new file mode 100644 index 00000000..4ac4dc1f --- /dev/null +++ b/tests/Tests/GenericHost.Test/Mocks/BackgroundOutboxDelivery.cs @@ -0,0 +1,33 @@ +namespace SpaceEngineers.Core.GenericHost.Test.Mocks +{ + using System; + using System.Collections.Generic; + using System.Threading; + using System.Threading.Tasks; + using AutoRegistration.Api.Abstractions; + using AutoRegistration.Api.Attributes; + using GenericEndpoint.Messaging; + using GenericEndpoint.UnitOfWork; + using IntegrationUnitOfWork = GenericEndpoint.DataAccess.Sql.UnitOfWork.IntegrationUnitOfWork; + + [ComponentOverride] + internal class BackgroundOutboxDelivery : IOutboxDelivery, + IDecorator + { + public BackgroundOutboxDelivery(IOutboxDelivery decoratee) + { + Decoratee = decoratee; + } + + public IOutboxDelivery Decoratee { get; } + + public Task DeliverMessages( + IReadOnlyCollection messages, + CancellationToken token) + { + return Environment.StackTrace.Contains(nameof(IntegrationUnitOfWork), StringComparison.OrdinalIgnoreCase) + ? Task.CompletedTask + : Decoratee.DeliverMessages(messages, token); + } + } +} \ No newline at end of file diff --git a/tests/Tests/GenericHost.Test/Mocks/QueryExpressionsCollector.cs b/tests/Tests/GenericHost.Test/Mocks/QueryExpressionsCollector.cs new file mode 100644 index 00000000..b997ed20 --- /dev/null +++ b/tests/Tests/GenericHost.Test/Mocks/QueryExpressionsCollector.cs @@ -0,0 +1,39 @@ +namespace SpaceEngineers.Core.GenericHost.Test.Mocks +{ + using System; + using System.Collections.Concurrent; + using System.Linq.Expressions; + using AutoRegistration.Api.Abstractions; + using AutoRegistration.Api.Attributes; + using DataAccess.Orm.Sql.Linq; + + [ManuallyRegisteredComponent(nameof(LinqToSqlTests))] + internal class QueryExpressionsCollector : IResolvable, + IDisposable + { + private readonly IAsyncQueryProvider _queryProvider; + private readonly EventHandler _subscription; + + public QueryExpressionsCollector(IAsyncQueryProvider queryProvider) + { + _queryProvider = queryProvider; + _subscription = Collect; + + _queryProvider.ExpressionExecuted += _subscription; + + Expressions = new ConcurrentQueue(); + } + + public ConcurrentQueue Expressions { get; } + + public void Dispose() + { + _queryProvider.ExpressionExecuted -= _subscription; + } + + private void Collect(object? sender, ExecutedExpressionEventArgs args) + { + Expressions.Enqueue(args.Expression); + } + } +} \ No newline at end of file diff --git a/tests/Tests/GenericHost.Test/Mocks/TestMessagesCollector.cs b/tests/Tests/GenericHost.Test/Mocks/TestMessagesCollector.cs new file mode 100644 index 00000000..caf262de --- /dev/null +++ b/tests/Tests/GenericHost.Test/Mocks/TestMessagesCollector.cs @@ -0,0 +1,210 @@ +namespace SpaceEngineers.Core.GenericHost.Test.Mocks +{ + using System; + using System.Collections.Concurrent; + using System.Threading.Tasks; + using AutoRegistration.Api.Abstractions; + using AutoRegistration.Api.Attributes; + using Basics.Primitives; + using CrossCuttingConcerns.Logging; + using GenericEndpoint.Contract.Abstractions; + using GenericEndpoint.Messaging; + using IntegrationTransport.Api.Abstractions; + using Microsoft.Extensions.Logging; + + [ManuallyRegisteredComponent(nameof(EndpointCommunicationTests))] + internal class TestMessagesCollector : IResolvable, + IDisposable + { + private readonly IExecutableIntegrationTransport _integrationTransport; + private readonly ILogger _logger; + + private readonly EventHandler _subscription; + + public TestMessagesCollector( + IExecutableIntegrationTransport integrationTransport, + ILogger logger) + { + _integrationTransport = integrationTransport; + _logger = logger; + + _subscription = Collect; + + integrationTransport.MessageReceived += _subscription; + + Messages = new ConcurrentQueue(); + ErrorMessages = new ConcurrentQueue<(IntegrationMessage, Exception?)>(); + } + + private event EventHandler? OnCollected; + + public ConcurrentQueue Messages { get; } + + public ConcurrentQueue<(IntegrationMessage message, Exception? exception)> ErrorMessages { get; } + + public void Dispose() + { + _integrationTransport.MessageReceived -= _subscription; + + Messages.Clear(); + ErrorMessages.Clear(); + } + + public async Task WaitUntilErrorMessageIsNotReceived( + Predicate? messagePredicate = null, + Predicate? exceptionPredicate = null) + { + var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + var subscription = MakeSubscription( + messagePredicate ?? (_ => true), + exceptionPredicate ?? (_ => true), + tcs); + + using (Disposable.Create((this, subscription), Subscribe, Unsubscribe)) + { + await tcs.Task.ConfigureAwait(false); + } + + static EventHandler MakeSubscription( + Predicate predicate, + Predicate exceptionPredicate, + TaskCompletionSource tcs) + { + return (_, eventArgs) => + { + if (eventArgs.Exception != null + && exceptionPredicate(eventArgs.Exception) + && predicate(eventArgs.Message)) + { + _ = tcs.TrySetResult(); + } + }; + } + } + + public async Task WaitUntilErrorMessageIsNotReceived( + Predicate? messagePredicate = null, + Predicate? exceptionPredicate = null) + where TMessage : IIntegrationMessage + { + var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + var subscription = MakeSubscription( + messagePredicate ?? (_ => true), + exceptionPredicate ?? (_ => true), + tcs); + + using (Disposable.Create((this, subscription), Subscribe, Unsubscribe)) + { + await tcs.Task.ConfigureAwait(false); + } + + static EventHandler MakeSubscription( + Predicate predicate, + Predicate exceptionPredicate, + TaskCompletionSource tcs) + { + return (_, eventArgs) => + { + if (eventArgs.Exception != null + && exceptionPredicate(eventArgs.Exception) + && eventArgs.Message.Payload is TMessage message + && predicate(message)) + { + _ = tcs.TrySetResult(); + } + }; + } + } + + public async Task WaitUntilMessageIsNotReceived(Predicate? predicate = null) + { + var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + var subscription = MakeSubscription(predicate ?? (_ => true), tcs); + + using (Disposable.Create((this, subscription), Subscribe, Unsubscribe)) + { + await tcs.Task.ConfigureAwait(false); + } + + static EventHandler MakeSubscription( + Predicate predicate, + TaskCompletionSource tcs) + { + return (_, eventArgs) => + { + if (predicate(eventArgs.Message)) + { + _ = tcs.TrySetResult(); + } + }; + } + } + + public async Task WaitUntilMessageIsNotReceived(Predicate? predicate = null) + where TMessage : IIntegrationMessage + { + var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + var subscription = MakeSubscription(predicate ?? (_ => true), tcs); + + using (Disposable.Create((this, subscription), Subscribe, Unsubscribe)) + { + await tcs.Task.ConfigureAwait(false); + } + + static EventHandler MakeSubscription( + Predicate predicate, + TaskCompletionSource tcs) + { + return (_, eventArgs) => + { + if (eventArgs.Message.Payload is TMessage message + && predicate(message)) + { + _ = tcs.TrySetResult(); + } + }; + } + } + + private void Collect(object? sender, IntegrationTransportMessageReceivedEventArgs args) + { + if (args.Exception == null) + { + _logger.Information($"Message has been collected: {args.Message.Payload.GetType().Name};"); + Messages.Enqueue(args.Message); + } + else + { + _logger.Information($"Failed message has been collected: {args.Message.Payload.GetType().Name};"); + ErrorMessages.Enqueue((args.Message, args.Exception)); + } + + OnCollected?.Invoke(this, new MessageCollectedEventArgs(args.Message, args.Exception)); + } + + private static void Subscribe((TestMessagesCollector, EventHandler) state) + { + var (collector, subscription) = state; + collector.OnCollected += subscription; + } + + private static void Unsubscribe((TestMessagesCollector, EventHandler) state) + { + var (collector, subscription) = state; + collector.OnCollected -= subscription; + } + + private class MessageCollectedEventArgs : EventArgs + { + public MessageCollectedEventArgs(IntegrationMessage message, Exception? exception) + { + Message = message; + Exception = exception; + } + + public IntegrationMessage Message { get; } + + public Exception? Exception { get; } + } + } +} \ No newline at end of file diff --git a/tests/Tests/GenericHost.Test/Mocks/TestRabbitMqSettingsProviderDecorator.cs b/tests/Tests/GenericHost.Test/Mocks/TestRabbitMqSettingsProviderDecorator.cs new file mode 100644 index 00000000..1166dbb8 --- /dev/null +++ b/tests/Tests/GenericHost.Test/Mocks/TestRabbitMqSettingsProviderDecorator.cs @@ -0,0 +1,47 @@ +namespace SpaceEngineers.Core.GenericHost.Test.Mocks +{ + using System.Reflection; + using AutoRegistration.Api.Abstractions; + using AutoRegistration.Api.Attributes; + using CrossCuttingConcerns.Settings; + using IntegrationTransport.RabbitMQ.Settings; + + [ManuallyRegisteredComponent(nameof(EndpointDataAccessTests))] + internal class TestRabbitMqSettingsProviderDecorator : ISettingsProvider, + IDecorator> + { + private readonly VirtualHostProvider _virtualHostProvider; + + public TestRabbitMqSettingsProviderDecorator( + ISettingsProvider decoratee, + VirtualHostProvider virtualHostProvider) + { + Decoratee = decoratee; + _virtualHostProvider = virtualHostProvider; + } + + public ISettingsProvider Decoratee { get; } + + public RabbitMqSettings Get() + { + var rabbitMqSettings = Decoratee.Get(); + + typeof(RabbitMqSettings) + .GetProperty(nameof(RabbitMqSettings.VirtualHost), BindingFlags.Public | BindingFlags.Instance | BindingFlags.SetProperty) + .SetValue(rabbitMqSettings, _virtualHostProvider.VirtualHost); + + return rabbitMqSettings; + } + + [ManuallyRegisteredComponent(nameof(EndpointDataAccessTests))] + internal class VirtualHostProvider : IResolvable + { + public VirtualHostProvider(string virtualHost) + { + VirtualHost = virtualHost; + } + + public string VirtualHost { get; } + } + } +} \ No newline at end of file diff --git a/tests/Tests/GenericHost.Test/Mocks/TestRetryPolicy.cs b/tests/Tests/GenericHost.Test/Mocks/TestRetryPolicy.cs new file mode 100644 index 00000000..0bca1230 --- /dev/null +++ b/tests/Tests/GenericHost.Test/Mocks/TestRetryPolicy.cs @@ -0,0 +1,33 @@ +namespace SpaceEngineers.Core.GenericHost.Test.Mocks +{ + using System; + using System.Threading; + using System.Threading.Tasks; + using AutoRegistration.Api.Abstractions; + using AutoRegistration.Api.Attributes; + using GenericEndpoint.Messaging.MessageHeaders; + using GenericEndpoint.Pipeline; + + [ComponentOverride] + internal class TestRetryPolicy : IRetryPolicy, + IResolvable + { + private static readonly int[] Scale = new[] { 0, 1, 2 }; + + public Task Apply( + IAdvancedIntegrationContext context, + Exception exception, + CancellationToken token) + { + var actualCounter = context.Message.ReadHeader()?.Value ?? 0; + + if (actualCounter < Scale.Length) + { + var dueTime = TimeSpan.FromSeconds(Scale[actualCounter]); + return context.Retry(dueTime, token); + } + + return context.Reject(exception, token); + } + } +} \ No newline at end of file diff --git a/tests/Tests/GenericHost.Test/Mocks/TestSqlDatabaseSettingsProviderDecorator.cs b/tests/Tests/GenericHost.Test/Mocks/TestSqlDatabaseSettingsProviderDecorator.cs new file mode 100644 index 00000000..97313c11 --- /dev/null +++ b/tests/Tests/GenericHost.Test/Mocks/TestSqlDatabaseSettingsProviderDecorator.cs @@ -0,0 +1,48 @@ +namespace SpaceEngineers.Core.GenericHost.Test.Mocks +{ + using System.Data; + using System.Reflection; + using AutoRegistration.Api.Abstractions; + using AutoRegistration.Api.Attributes; + using CrossCuttingConcerns.Settings; + using DataAccess.Orm.Sql.Settings; + + [ManuallyRegisteredComponent(nameof(EndpointDataAccessTests))] + internal class TestSqlDatabaseSettingsProviderDecorator : ISettingsProvider, + IDecorator> + { + private readonly IsolationLevelProvider _isolationLevelProvider; + + public TestSqlDatabaseSettingsProviderDecorator( + ISettingsProvider decoratee, + IsolationLevelProvider isolationLevelProvider) + { + Decoratee = decoratee; + _isolationLevelProvider = isolationLevelProvider; + } + + public ISettingsProvider Decoratee { get; } + + public SqlDatabaseSettings Get() + { + var sqlDatabaseSettings = Decoratee.Get(); + + typeof(SqlDatabaseSettings) + .GetProperty(nameof(SqlDatabaseSettings.IsolationLevel), BindingFlags.Public | BindingFlags.Instance | BindingFlags.SetProperty) + .SetValue(sqlDatabaseSettings, _isolationLevelProvider.IsolationLevel); + + return sqlDatabaseSettings; + } + + [ManuallyRegisteredComponent(nameof(EndpointDataAccessTests))] + internal class IsolationLevelProvider : IResolvable + { + public IsolationLevelProvider(IsolationLevel isolationLevel) + { + IsolationLevel = isolationLevel; + } + + public IsolationLevel IsolationLevel { get; } + } + } +} \ No newline at end of file diff --git a/tests/Tests/GenericHost.Test/Properties/AssemblyInfo.cs b/tests/Tests/GenericHost.Test/Properties/AssemblyInfo.cs new file mode 100644 index 00000000..8ea72966 --- /dev/null +++ b/tests/Tests/GenericHost.Test/Properties/AssemblyInfo.cs @@ -0,0 +1,8 @@ +using System.Reflection; +using Xunit; + +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] +[assembly: AssemblyInformationalVersion("1.0.0.0")] + +[assembly: CollectionBehavior(CollectionBehavior.CollectionPerAssembly)] \ No newline at end of file diff --git a/tests/Tests/GenericHost.Test/Registrations/BackgroundOutboxDeliveryManualRegistration.cs b/tests/Tests/GenericHost.Test/Registrations/BackgroundOutboxDeliveryManualRegistration.cs new file mode 100644 index 00000000..5fbed72f --- /dev/null +++ b/tests/Tests/GenericHost.Test/Registrations/BackgroundOutboxDeliveryManualRegistration.cs @@ -0,0 +1,15 @@ +namespace SpaceEngineers.Core.GenericHost.Test.Registrations +{ + using AutoRegistration.Api.Enumerations; + using CompositionRoot.Registration; + using GenericEndpoint.UnitOfWork; + using Mocks; + + internal class BackgroundOutboxDeliveryManualRegistration : IManualRegistration + { + public void Register(IManualRegistrationsContainer container) + { + container.RegisterDecorator(EnLifestyle.Singleton); + } + } +} \ No newline at end of file diff --git a/tests/Tests/GenericHost.Test/Registrations/IsolationLevelManualRegistration.cs b/tests/Tests/GenericHost.Test/Registrations/IsolationLevelManualRegistration.cs new file mode 100644 index 00000000..b1b8c0b8 --- /dev/null +++ b/tests/Tests/GenericHost.Test/Registrations/IsolationLevelManualRegistration.cs @@ -0,0 +1,25 @@ +namespace SpaceEngineers.Core.GenericHost.Test.Registrations +{ + using System.Data; + using AutoRegistration.Api.Enumerations; + using CompositionRoot.Registration; + using CrossCuttingConcerns.Settings; + using DataAccess.Orm.Sql.Settings; + using Mocks; + + internal class IsolationLevelManualRegistration : IManualRegistration + { + private readonly IsolationLevel _isolationLevel; + + public IsolationLevelManualRegistration(IsolationLevel isolationLevel) + { + _isolationLevel = isolationLevel; + } + + public void Register(IManualRegistrationsContainer container) + { + container.RegisterInstance(new TestSqlDatabaseSettingsProviderDecorator.IsolationLevelProvider(_isolationLevel)); + container.RegisterDecorator, TestSqlDatabaseSettingsProviderDecorator>(EnLifestyle.Singleton); + } + } +} \ No newline at end of file diff --git a/tests/Tests/GenericHost.Test/Registrations/MessagesCollectorManualRegistration.cs b/tests/Tests/GenericHost.Test/Registrations/MessagesCollectorManualRegistration.cs new file mode 100644 index 00000000..912f8ee8 --- /dev/null +++ b/tests/Tests/GenericHost.Test/Registrations/MessagesCollectorManualRegistration.cs @@ -0,0 +1,14 @@ +namespace SpaceEngineers.Core.GenericHost.Test.Registrations +{ + using AutoRegistration.Api.Enumerations; + using CompositionRoot.Registration; + using Mocks; + + internal class MessagesCollectorManualRegistration : IManualRegistration + { + public void Register(IManualRegistrationsContainer container) + { + container.Register(EnLifestyle.Scoped); + } + } +} \ No newline at end of file diff --git a/tests/Tests/GenericHost.Test/Registrations/PurgeRabbitMqQueuesManualRegistration.cs b/tests/Tests/GenericHost.Test/Registrations/PurgeRabbitMqQueuesManualRegistration.cs new file mode 100644 index 00000000..128fa54b --- /dev/null +++ b/tests/Tests/GenericHost.Test/Registrations/PurgeRabbitMqQueuesManualRegistration.cs @@ -0,0 +1,16 @@ +namespace SpaceEngineers.Core.GenericHost.Test.Registrations +{ + using AutoRegistration.Api.Enumerations; + using CompositionRoot.Registration; + using StartupActions; + + internal class PurgeRabbitMqQueuesManualRegistration : IManualRegistration + { + public void Register(IManualRegistrationsContainer container) + { + container.Register(EnLifestyle.Singleton); + container.Advanced.RegisterCollectionEntry(EnLifestyle.Singleton); + container.Advanced.RegisterCollectionEntry(EnLifestyle.Singleton); + } + } +} \ No newline at end of file diff --git a/tests/Tests/GenericHost.Test/Registrations/QueryExpressionsCollectorManualRegistration.cs b/tests/Tests/GenericHost.Test/Registrations/QueryExpressionsCollectorManualRegistration.cs new file mode 100644 index 00000000..f77b29fa --- /dev/null +++ b/tests/Tests/GenericHost.Test/Registrations/QueryExpressionsCollectorManualRegistration.cs @@ -0,0 +1,14 @@ +namespace SpaceEngineers.Core.GenericHost.Test.Registrations +{ + using AutoRegistration.Api.Enumerations; + using CompositionRoot.Registration; + using Mocks; + + internal class QueryExpressionsCollectorManualRegistration : IManualRegistration + { + public void Register(IManualRegistrationsContainer container) + { + container.Register(EnLifestyle.Scoped); + } + } +} \ No newline at end of file diff --git a/tests/Tests/GenericHost.Test/Registrations/VirtualHostManualRegistration.cs b/tests/Tests/GenericHost.Test/Registrations/VirtualHostManualRegistration.cs new file mode 100644 index 00000000..384cb12a --- /dev/null +++ b/tests/Tests/GenericHost.Test/Registrations/VirtualHostManualRegistration.cs @@ -0,0 +1,24 @@ +namespace SpaceEngineers.Core.GenericHost.Test.Registrations +{ + using AutoRegistration.Api.Enumerations; + using CompositionRoot.Registration; + using CrossCuttingConcerns.Settings; + using IntegrationTransport.RabbitMQ.Settings; + using Mocks; + + internal class VirtualHostManualRegistration : IManualRegistration + { + private readonly string _virtualHost; + + public VirtualHostManualRegistration(string virtualHost) + { + _virtualHost = virtualHost; + } + + public void Register(IManualRegistrationsContainer container) + { + container.RegisterInstance(new TestRabbitMqSettingsProviderDecorator.VirtualHostProvider(_virtualHost)); + container.RegisterDecorator, TestRabbitMqSettingsProviderDecorator>(EnLifestyle.Singleton); + } + } +} \ No newline at end of file diff --git a/tests/Tests/GenericHost.Test/RunHostTest.cs b/tests/Tests/GenericHost.Test/RunHostTest.cs new file mode 100644 index 00000000..7ade1a40 --- /dev/null +++ b/tests/Tests/GenericHost.Test/RunHostTest.cs @@ -0,0 +1,597 @@ +namespace SpaceEngineers.Core.GenericHost.Test +{ + using System; + using System.Collections.Generic; + using System.Diagnostics.CodeAnalysis; + using System.Globalization; + using System.IO; + using System.Linq; + using System.Threading; + using System.Threading.Tasks; + using AutoRegistration.Api.Enumerations; + using Basics; + using CrossCuttingConcerns.Settings; + using Extensions; + using GenericEndpoint.Host; + using GenericEndpoint.Messaging; + using GenericEndpoint.Messaging.MessageHeaders; + using GenericEndpoint.Pipeline; + using GenericHost; + using IntegrationTransport.Api; + using IntegrationTransport.Api.Abstractions; + using IntegrationTransport.Host; + using IntegrationTransport.RabbitMQ; + using IntegrationTransport.RabbitMQ.Settings; + using MessageHandlers; + using Messages; + using Microsoft.Extensions.Hosting; + using Mocks; + using Registrations; + using SpaceEngineers.Core.Test.Api; + using SpaceEngineers.Core.Test.Api.ClassFixtures; + using Xunit; + using Xunit.Abstractions; + using EventHandler = MessageHandlers.EventHandler; + + /// + /// Endpoint communication tests + /// + [SuppressMessage("Analysis", "CA1506", Justification = "application composition root")] + public class EndpointCommunicationTests : TestBase + { + /// .cctor + /// ITestOutputHelper + /// TestFixture + public EndpointCommunicationTests(ITestOutputHelper output, TestFixture fixture) + : base(output, fixture) + { + } + + /// + /// Test cases for endpoint communication tests + /// + /// Test cases + public static IEnumerable EndpointCommunicationTestData() + { + Func settingsDirectoryProducer = + testDirectory => + { + var projectFileDirectory = SolutionExtensions.ProjectFile().Directory + ?? throw new InvalidOperationException("Project directory wasn't found"); + + return projectFileDirectory + .StepInto("Settings") + .StepInto(testDirectory); + }; + + var inMemoryIntegrationTransportIdentity = IntegrationTransport.InMemory.Identity.TransportIdentity(); + + var useInMemoryIntegrationTransport = new Func( + static (hostBuilder, transportIdentity) => hostBuilder.UseInMemoryIntegrationTransport( + transportIdentity, + options => options + .WithManualRegistrations(new MessagesCollectorManualRegistration()))); + + var rabbitMqIntegrationTransportIdentity = IntegrationTransport.RabbitMQ.Identity.TransportIdentity(); + + var useRabbitMqIntegrationTransport = new Func( + static (hostBuilder, transportIdentity) => hostBuilder.UseRabbitMqIntegrationTransport( + transportIdentity, + builder => builder + .WithManualRegistrations(new PurgeRabbitMqQueuesManualRegistration()) + .WithManualRegistrations(new MessagesCollectorManualRegistration()))); + + var integrationTransportProviders = new[] + { + (inMemoryIntegrationTransportIdentity, useInMemoryIntegrationTransport), + (rabbitMqIntegrationTransportIdentity, useRabbitMqIntegrationTransport) + }; + + return integrationTransportProviders + .Select(transport => + { + var (transportIdentity, useTransport) = transport; + + return new object[] + { + settingsDirectoryProducer, + transportIdentity, + useTransport + }; + }); + } + + [Theory(Timeout = 60_000)] + [MemberData(nameof(EndpointCommunicationTestData))] + internal async Task Endpoint_supports_request_reply_communication_pattern( + Func settingsDirectoryProducer, + TransportIdentity transportIdentity, + Func useTransport) + { + var settingsDirectory = settingsDirectoryProducer("EndpointSupportsRequestReplyCommunicationPattern"); + + var messageTypes = new[] + { + typeof(MakeRequestCommand), + typeof(Request), + typeof(Reply), + typeof(HandlerInvoked) + }; + + var messageHandlerTypes = new[] + { + typeof(MakeRequestCommandHandler), + typeof(AlwaysReplyRequestHandler), + typeof(ReplyHandler) + }; + + var additionalOurTypes = messageTypes.Concat(messageHandlerTypes).ToArray(); + + var host = Fixture + .CreateHostBuilder() + .UseIntegrationTransport(transportIdentity, useTransport) + .UseEndpoint(TestIdentity.Endpoint10, + builder => builder + .ModifyContainerOptions(options => options + .WithAdditionalOurTypes(additionalOurTypes)) + .BuildOptions()) + .BuildHost(settingsDirectory); + + await host + .RunTestHost(Output, TestCase, RequestReplyTestInternal(settingsDirectory, transportIdentity)) + .ConfigureAwait(false); + + static Func RequestReplyTestInternal( + DirectoryInfo settingsDirectory, + TransportIdentity transportIdentity) + { + return async (_, host, token) => + { + var transportDependencyContainer = host.GetIntegrationTransportDependencyContainer(transportIdentity); + var endpointDependencyContainer = host.GetEndpointDependencyContainer(TestIdentity.Endpoint10); + + if (transportDependencyContainer.Resolve() is RabbitMqIntegrationTransport) + { + var rabbitMqSettings = transportDependencyContainer + .Resolve>() + .Get(); + + Assert.Equal(settingsDirectory.Name, rabbitMqSettings.VirtualHost); + } + + await using (transportDependencyContainer.OpenScopeAsync().ConfigureAwait(false)) + { + var collector = transportDependencyContainer.Resolve(); + + var command = new MakeRequestCommand(42); + + var awaiter = Task.WhenAll( + collector.WaitUntilMessageIsNotReceived(message => message.Id == command.Id), + collector.WaitUntilMessageIsNotReceived(message => message.Id == command.Id), + collector.WaitUntilMessageIsNotReceived(message => message.Id == command.Id), + collector.WaitUntilErrorMessageIsNotReceived(message => message.HandlerType == typeof(ReplyHandler) && message.EndpointIdentity == TestIdentity.Endpoint10)); + + var integrationMessage = endpointDependencyContainer + .Resolve() + .CreateGeneralMessage( + command, + typeof(MakeRequestCommand), + Array.Empty(), + null); + + await transportDependencyContainer + .Resolve() + .Enqueue(integrationMessage, token) + .ConfigureAwait(false); + + await awaiter.ConfigureAwait(false); + } + }; + } + } + + // TODO: #205 - implement rpc-transport + [SuppressMessage("Analysis", "xUnit1004", Justification = "#205")] + [Theory(Skip = "#205", Timeout = 60_000)] + [MemberData(nameof(EndpointCommunicationTestData))] + internal async Task Endpoint_supports_remote_procedure_calls( + Func settingsDirectoryProducer, + TransportIdentity transportIdentity, + Func useTransport) + { + var settingsDirectory = settingsDirectoryProducer("EndpointSupportsRemoteProcedureCalls"); + + var messageTypes = new[] + { + typeof(MakeRpcRequestCommand), + typeof(Request), + typeof(Reply), + typeof(HandlerInvoked) + }; + + var messageHandlerTypes = new[] + { + typeof(MakeRpcRequestCommandHandler), + typeof(AlwaysReplyRequestHandler) + }; + + var additionalOurTypes = messageTypes.Concat(messageHandlerTypes).ToArray(); + + var host = Fixture + .CreateHostBuilder() + .UseIntegrationTransport(transportIdentity, useTransport) + .UseEndpoint(TestIdentity.Endpoint10, + builder => builder + .ModifyContainerOptions(options => options + .WithAdditionalOurTypes(additionalOurTypes)) + .BuildOptions()) + .BuildHost(settingsDirectory); + + await host + .RunTestHost(Output, TestCase, RpcRequestTestInternal(settingsDirectory, transportIdentity)) + .ConfigureAwait(false); + + static Func RpcRequestTestInternal( + DirectoryInfo settingsDirectory, + TransportIdentity transportIdentity) + { + return async (_, host, token) => + { + var transportDependencyContainer = host.GetIntegrationTransportDependencyContainer(transportIdentity); + var endpointDependencyContainer = host.GetEndpointDependencyContainer(TestIdentity.Endpoint10); + + if (transportDependencyContainer.Resolve() is RabbitMqIntegrationTransport) + { + var rabbitMqSettings = transportDependencyContainer + .Resolve>() + .Get(); + + Assert.Equal(settingsDirectory.Name, rabbitMqSettings.VirtualHost); + } + + await using (transportDependencyContainer.OpenScopeAsync().ConfigureAwait(false)) + { + var collector = transportDependencyContainer.Resolve(); + + var command = new MakeRpcRequestCommand(42); + + var integrationMessage = endpointDependencyContainer + .Resolve() + .CreateGeneralMessage( + command, + typeof(MakeRpcRequestCommand), + Array.Empty(), + null); + + var awaiter = Task.WhenAll( + collector.WaitUntilMessageIsNotReceived(message => message.Id == command.Id), + collector.WaitUntilMessageIsNotReceived(message => message.ReflectedType == typeof(Request) && ((Request)message.Payload).Id == command.Id && message.ReadRequiredHeader().Value.LogicalName.Equals(TestIdentity.Endpoint10.LogicalName, StringComparison.OrdinalIgnoreCase)), + collector.WaitUntilMessageIsNotReceived(message => message.ReflectedType == typeof(Reply) && ((Reply)message.Payload).Id == command.Id && message.ReadRequiredHeader().Value.Equals(integrationMessage.ReadRequiredHeader().Value)), + collector.WaitUntilErrorMessageIsNotReceived(message => message.HandlerType == typeof(MakeRpcRequestCommandHandler) && message.EndpointIdentity == TestIdentity.Endpoint10)); + + await transportDependencyContainer + .Resolve() + .Enqueue(integrationMessage, token) + .ConfigureAwait(false); + + await awaiter.ConfigureAwait(false); + } + }; + } + } + + [Theory(Timeout = 60_000)] + [MemberData(nameof(EndpointCommunicationTestData))] + internal async Task Endpoint_supports_contravariant_messaging( + Func settingsDirectoryProducer, + TransportIdentity transportIdentity, + Func useTransport) + { + var settingsDirectory = settingsDirectoryProducer("EndpointSupportsContravariantMessaging"); + + var messageTypes = new[] + { + typeof(PublishInheritedEventCommand), + typeof(BaseEvent), + typeof(InheritedEvent), + typeof(HandlerInvoked) + }; + + var messageHandlerTypes = new[] + { + typeof(PublishInheritedEventCommandHandler), + typeof(BaseEventHandler), + typeof(InheritedEventHandler) + }; + + var additionalOurTypes = messageTypes.Concat(messageHandlerTypes).ToArray(); + + var host = Fixture + .CreateHostBuilder() + .UseIntegrationTransport(transportIdentity, useTransport) + .UseEndpoint(TestIdentity.Endpoint10, + builder => builder + .ModifyContainerOptions(options => options + .WithAdditionalOurTypes(additionalOurTypes)) + .BuildOptions()) + .BuildHost(settingsDirectory); + + await host + .RunTestHost(Output, TestCase, ContravariantMessageHandlerTestInternal(settingsDirectory, transportIdentity)) + .ConfigureAwait(false); + + static Func ContravariantMessageHandlerTestInternal( + DirectoryInfo settingsDirectory, + TransportIdentity transportIdentity) + { + return async (_, host, token) => + { + var transportDependencyContainer = host.GetIntegrationTransportDependencyContainer(transportIdentity); + var endpointDependencyContainer = host.GetEndpointDependencyContainer(TestIdentity.Endpoint10); + + if (transportDependencyContainer.Resolve() is RabbitMqIntegrationTransport) + { + var rabbitMqSettings = transportDependencyContainer + .Resolve>() + .Get(); + + Assert.Equal(settingsDirectory.Name, rabbitMqSettings.VirtualHost); + } + + await using (transportDependencyContainer.OpenScopeAsync().ConfigureAwait(false)) + { + var collector = transportDependencyContainer.Resolve(); + + var awaiter = Task.WhenAll( + collector.WaitUntilMessageIsNotReceived(message => message.ReflectedType == typeof(PublishInheritedEventCommand)), + collector.WaitUntilMessageIsNotReceived(message => message.Payload.GetType() == typeof(InheritedEvent) && message.ReflectedType == typeof(BaseEvent)), + collector.WaitUntilMessageIsNotReceived(message => message.Payload.GetType() == typeof(InheritedEvent) && message.ReflectedType == typeof(InheritedEvent)), + collector.WaitUntilErrorMessageIsNotReceived(message => message.HandlerType == typeof(BaseEventHandler) && message.EndpointIdentity == TestIdentity.Endpoint10), + collector.WaitUntilErrorMessageIsNotReceived(message => message.HandlerType == typeof(InheritedEventHandler) && message.EndpointIdentity == TestIdentity.Endpoint10)); + + var integrationMessage = endpointDependencyContainer + .Resolve() + .CreateGeneralMessage( + new PublishInheritedEventCommand(42), + typeof(PublishInheritedEventCommand), + Array.Empty(), + null); + + await transportDependencyContainer + .Resolve() + .Enqueue(integrationMessage, token) + .ConfigureAwait(false); + + await awaiter.ConfigureAwait(false); + } + }; + } + } + + [Theory(Timeout = 60_000)] + [MemberData(nameof(EndpointCommunicationTestData))] + internal async Task Endpoint_supports_custom_retry_policies( + Func settingsDirectoryProducer, + TransportIdentity transportIdentity, + Func useTransport) + { + var settingsDirectory = settingsDirectoryProducer("EndpointSupportsCustomRetryPolicies"); + + var messageTypes = new[] + { + typeof(Command) + }; + + var messageHandlerTypes = new[] + { + typeof(ThrowingCommandHandler) + }; + + var additionalOurTypes = messageTypes.Concat(messageHandlerTypes).ToArray(); + + var host = Fixture + .CreateHostBuilder() + .UseIntegrationTransport(transportIdentity, useTransport) + .UseEndpoint(TestIdentity.Endpoint10, + builder => builder + .ModifyContainerOptions(options => options + .WithAdditionalOurTypes(additionalOurTypes) + .WithOverrides(Fixture.DelegateOverride(container => container + .Override(EnLifestyle.Singleton)))) + .BuildOptions()) + .BuildHost(settingsDirectory); + + await host + .RunTestHost(Output, TestCase, ThrowingMessageHandlerTestInternal(settingsDirectory, transportIdentity)) + .ConfigureAwait(false); + + static Func ThrowingMessageHandlerTestInternal( + DirectoryInfo settingsDirectory, + TransportIdentity transportIdentity) + { + return async (output, host, token) => + { + var transportDependencyContainer = host.GetIntegrationTransportDependencyContainer(transportIdentity); + var endpointDependencyContainer = host.GetEndpointDependencyContainer(TestIdentity.Endpoint10); + + if (transportDependencyContainer.Resolve() is RabbitMqIntegrationTransport) + { + var rabbitMqSettings = transportDependencyContainer + .Resolve>() + .Get(); + + Assert.Equal(settingsDirectory.Name, rabbitMqSettings.VirtualHost); + } + + await using (transportDependencyContainer.OpenScopeAsync().ConfigureAwait(false)) + { + var collector = transportDependencyContainer.Resolve(); + + var awaiter = collector.WaitUntilErrorMessageIsNotReceived(); + + var integrationMessage = endpointDependencyContainer + .Resolve() + .CreateGeneralMessage( + new Command(42), + typeof(Command), + Array.Empty(), + null); + + await transportDependencyContainer + .Resolve() + .Enqueue(integrationMessage, token) + .ConfigureAwait(false); + + await awaiter.ConfigureAwait(false); + + Assert.Single(collector.ErrorMessages); + var (errorMessage, exception) = collector.ErrorMessages.Single(); + Assert.Equal(42.ToString(CultureInfo.InvariantCulture), exception.Message); + Assert.Equal(3, errorMessage.ReadHeader()?.Value); + + var expectedRetryCounters = new[] { 0, 1, 2, 3 }; + var actualRetryCounters = collector + .Messages + .Select(message => message.ReadHeader()?.Value ?? default(int)) + .ToList(); + + Assert.Equal(expectedRetryCounters, actualRetryCounters); + + var actualDeliveries = collector + .Messages + .Select(message => message.ReadRequiredHeader().Value) + .ToList(); + + var expectedDeliveryDelays = new[] + { + 0, + 1000, + 2000 + }; + + var actualDeliveryDelays = actualDeliveries + .Zip(actualDeliveries.Skip(1)) + .Select(period => period.Second - period.First) + .Select(span => span.TotalMilliseconds) + .ToList(); + + Assert.Equal(actualDeliveryDelays.Count, expectedDeliveryDelays.Length); + + Assert.True(actualDeliveryDelays + .Zip(expectedDeliveryDelays, (actual, expected) => ((int)actual, expected)) + .All(delays => + { + var (actual, expected) = delays; + output.WriteLine($"{0.95 * expected} ({0.95} * {expected}) <= {actual}"); + return 0.95 * expected <= actual; + })); + } + }; + } + } + + [Theory(Timeout = 60_000)] + [MemberData(nameof(EndpointCommunicationTestData))] + internal async Task Endpoint_supports_publish_subscribe_communication_pattern( + Func settingsDirectoryProducer, + TransportIdentity transportIdentity, + Func useTransport) + { + var settingsDirectory = settingsDirectoryProducer("EndpointSupportsPublishSubscribeCommunicationPattern"); + + var endpoint1MessageTypes = new[] + { + typeof(Event), + typeof(HandlerInvoked) + }; + + var endpoint1MessageHandlerTypes = new[] + { + typeof(EventHandler) + }; + + var endpoint2MessageTypes = new[] + { + typeof(Event), + typeof(PublishEventCommand), + typeof(HandlerInvoked) + }; + + var endpoint2MessageHandlerTypes = new[] + { + typeof(PublishEventCommandHandler), + typeof(EventHandler) + }; + + var endpoint1AdditionalOurTypes = endpoint1MessageTypes.Concat(endpoint1MessageHandlerTypes).ToArray(); + var endpoint2AdditionalOurTypes = endpoint2MessageTypes.Concat(endpoint2MessageHandlerTypes).ToArray(); + + var host = Fixture + .CreateHostBuilder() + .UseIntegrationTransport(transportIdentity, useTransport) + .UseEndpoint(TestIdentity.Endpoint10, + builder => builder + .ModifyContainerOptions(options => options + .WithAdditionalOurTypes(endpoint1AdditionalOurTypes)) + .BuildOptions()) + .UseEndpoint(TestIdentity.Endpoint20, + builder => builder + .ModifyContainerOptions(options => options + .WithAdditionalOurTypes(endpoint2AdditionalOurTypes)) + .BuildOptions()) + .BuildHost(settingsDirectory); + + await host + .RunTestHost(Output, TestCase, EventSubscriptionBetweenEndpointsTestInternal(settingsDirectory, transportIdentity)) + .ConfigureAwait(false); + + static Func EventSubscriptionBetweenEndpointsTestInternal( + DirectoryInfo settingsDirectory, + TransportIdentity transportIdentity) + { + return async (_, host, token) => + { + var transportDependencyContainer = host.GetIntegrationTransportDependencyContainer(transportIdentity); + var endpointDependencyContainer = host.GetEndpointDependencyContainer(TestIdentity.Endpoint10); + + if (transportDependencyContainer.Resolve() is RabbitMqIntegrationTransport) + { + var rabbitMqSettings = transportDependencyContainer + .Resolve>() + .Get(); + + Assert.Equal(settingsDirectory.Name, rabbitMqSettings.VirtualHost); + } + + await using (transportDependencyContainer.OpenScopeAsync().ConfigureAwait(false)) + { + var collector = transportDependencyContainer.Resolve(); + + var command = new PublishEventCommand(42); + + var awaiter = Task.WhenAll( + collector.WaitUntilMessageIsNotReceived(message => message.Id == command.Id), + collector.WaitUntilMessageIsNotReceived(message => message.Id == command.Id), + collector.WaitUntilErrorMessageIsNotReceived(message => message.HandlerType == typeof(EventHandler) && message.EndpointIdentity == TestIdentity.Endpoint10), + collector.WaitUntilErrorMessageIsNotReceived(message => message.HandlerType == typeof(EventHandler) && message.EndpointIdentity == TestIdentity.Endpoint20)); + + var integrationMessage = endpointDependencyContainer + .Resolve() + .CreateGeneralMessage( + command, + typeof(PublishEventCommand), + Array.Empty(), + null); + + await transportDependencyContainer + .Resolve() + .Enqueue(integrationMessage, token) + .ConfigureAwait(false); + + await awaiter.ConfigureAwait(false); + } + }; + } + } + } +} \ No newline at end of file diff --git a/tests/Tests/GenericHost.Test/Settings/EndpointAppliesOptimisticConcurrencyControl/appsettings.json b/tests/Tests/GenericHost.Test/Settings/EndpointAppliesOptimisticConcurrencyControl/appsettings.json new file mode 100644 index 00000000..f2c03403 --- /dev/null +++ b/tests/Tests/GenericHost.Test/Settings/EndpointAppliesOptimisticConcurrencyControl/appsettings.json @@ -0,0 +1,52 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Debug", + "System": "Information", + "Microsoft": "Information", + "Npgsql": "Information" + } + }, + "Transports": { + "RabbitMqIntegrationTransport": { + "RabbitMqSettings": { + "Hosts": [ + "localhost" + ], + "Port": "5672", + "HttpApiPort": "15672", + "User": "guest", + "Password": "guest", + "VirtualHost": "EndpointAppliesOptimisticConcurrencyControl", + "ApplicationName": "EndpointAppliesOptimisticConcurrencyControl", + "ConsumerPrefetchCount": 100, + "QueueMaxLengthBytes": 1048576, + "ConsumerPriority": 0 + } + }, + "InMemoryIntegrationTransport": { } + }, + "Endpoints":{ + "Endpoint1": { + "GenericEndpointSettings": { + "RpcRequestSecondsTimeout": 60 + }, + "OrmSettings": { + "CommandSecondsTimeout": 10 + }, + "OutboxSettings": { + "OutboxDeliverySecondsInterval": 60 + }, + "SqlDatabaseSettings": { + "ApplicationName": "EndpointAppliesOptimisticConcurrencyControl", + "Host": "localhost", + "Port": 5432, + "Database": "EndpointAppliesOptimisticConcurrencyControl", + "IsolationLevel": "ReadCommitted", + "Username": "postgres", + "Password": "Password12!", + "ConnectionPoolSize": 3 + } + } + } +} \ No newline at end of file diff --git a/tests/Tests/GenericHost.Test/Settings/EndpointSupportsContravariantMessaging/appsettings.json b/tests/Tests/GenericHost.Test/Settings/EndpointSupportsContravariantMessaging/appsettings.json new file mode 100644 index 00000000..c4e4ab46 --- /dev/null +++ b/tests/Tests/GenericHost.Test/Settings/EndpointSupportsContravariantMessaging/appsettings.json @@ -0,0 +1,36 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Debug", + "System": "Information", + "Microsoft": "Information", + "Npgsql": "Information" + } + }, + "Transports": { + "RabbitMqIntegrationTransport": { + "RabbitMqSettings": { + "Hosts": [ + "localhost" + ], + "Port": "5672", + "HttpApiPort": "15672", + "User": "guest", + "Password": "guest", + "VirtualHost": "EndpointSupportsContravariantMessaging", + "ApplicationName": "EndpointSupportsContravariantMessaging", + "ConsumerPrefetchCount": 100, + "QueueMaxLengthBytes": 1048576, + "ConsumerPriority": 0 + } + }, + "InMemoryIntegrationTransport": { } + }, + "Endpoints": { + "Endpoint1": { + "GenericEndpointSettings": { + "RpcRequestSecondsTimeout": 60 + } + } + } +} \ No newline at end of file diff --git a/tests/Tests/GenericHost.Test/Settings/EndpointSupportsCustomRetryPolicies/appsettings.json b/tests/Tests/GenericHost.Test/Settings/EndpointSupportsCustomRetryPolicies/appsettings.json new file mode 100644 index 00000000..cb7fa938 --- /dev/null +++ b/tests/Tests/GenericHost.Test/Settings/EndpointSupportsCustomRetryPolicies/appsettings.json @@ -0,0 +1,36 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Debug", + "System": "Information", + "Microsoft": "Information", + "Npgsql": "Information" + } + }, + "Transports": { + "RabbitMqIntegrationTransport": { + "RabbitMqSettings": { + "Hosts": [ + "localhost" + ], + "Port": "5672", + "HttpApiPort": "15672", + "User": "guest", + "Password": "guest", + "VirtualHost": "EndpointSupportsCustomRetryPolicies", + "ApplicationName": "EndpointSupportsCustomRetryPolicies", + "ConsumerPrefetchCount": 100, + "QueueMaxLengthBytes": 1048576, + "ConsumerPriority": 0 + } + }, + "InMemoryIntegrationTransport": { } + }, + "Endpoints": { + "Endpoint1": { + "GenericEndpointSettings": { + "RpcRequestSecondsTimeout": 60 + } + } + } +} \ No newline at end of file diff --git a/tests/Tests/GenericHost.Test/Settings/EndpointSupportsPublishSubscribeCommunicationPattern/appsettings.json b/tests/Tests/GenericHost.Test/Settings/EndpointSupportsPublishSubscribeCommunicationPattern/appsettings.json new file mode 100644 index 00000000..2074a4a7 --- /dev/null +++ b/tests/Tests/GenericHost.Test/Settings/EndpointSupportsPublishSubscribeCommunicationPattern/appsettings.json @@ -0,0 +1,41 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Debug", + "System": "Information", + "Microsoft": "Information", + "Npgsql": "Information" + } + }, + "Transports": { + "RabbitMqIntegrationTransport": { + "RabbitMqSettings": { + "Hosts": [ + "localhost" + ], + "Port": "5672", + "HttpApiPort": "15672", + "User": "guest", + "Password": "guest", + "VirtualHost": "EndpointSupportsPublishSubscribeCommunicationPattern", + "ApplicationName": "EndpointSupportsPublishSubscribeCommunicationPattern", + "ConsumerPrefetchCount": 100, + "QueueMaxLengthBytes": 1048576, + "ConsumerPriority": 0 + } + }, + "InMemoryIntegrationTransport": { } + }, + "Endpoints": { + "Endpoint1": { + "GenericEndpointSettings": { + "RpcRequestSecondsTimeout": 60 + } + }, + "Endpoint2": { + "GenericEndpointSettings": { + "RpcRequestSecondsTimeout": 60 + } + } + } +} \ No newline at end of file diff --git a/tests/Tests/GenericHost.Test/Settings/EndpointSupportsRemoteProcedureCalls/appsettings.json b/tests/Tests/GenericHost.Test/Settings/EndpointSupportsRemoteProcedureCalls/appsettings.json new file mode 100644 index 00000000..53317398 --- /dev/null +++ b/tests/Tests/GenericHost.Test/Settings/EndpointSupportsRemoteProcedureCalls/appsettings.json @@ -0,0 +1,36 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Debug", + "System": "Information", + "Microsoft": "Information", + "Npgsql": "Information" + } + }, + "Transports": { + "RabbitMqIntegrationTransport": { + "RabbitMqSettings": { + "Hosts": [ + "localhost" + ], + "Port": "5672", + "HttpApiPort": "15672", + "User": "guest", + "Password": "guest", + "VirtualHost": "EndpointSupportsRemoteProcedureCalls", + "ApplicationName": "EndpointSupportsRemoteProcedureCalls", + "ConsumerPrefetchCount": 100, + "QueueMaxLengthBytes": 1048576, + "ConsumerPriority": 0 + } + }, + "InMemoryIntegrationTransport": { } + }, + "Endpoints": { + "Endpoint1": { + "GenericEndpointSettings": { + "RpcRequestSecondsTimeout": 15 + } + } + } +} \ No newline at end of file diff --git a/tests/Tests/GenericHost.Test/Settings/EndpointSupportsRequestReplyCommunicationPattern/appsettings.json b/tests/Tests/GenericHost.Test/Settings/EndpointSupportsRequestReplyCommunicationPattern/appsettings.json new file mode 100644 index 00000000..215245f7 --- /dev/null +++ b/tests/Tests/GenericHost.Test/Settings/EndpointSupportsRequestReplyCommunicationPattern/appsettings.json @@ -0,0 +1,36 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Debug", + "System": "Information", + "Microsoft": "Information", + "Npgsql": "Information" + } + }, + "Transports": { + "RabbitMqIntegrationTransport": { + "RabbitMqSettings": { + "Hosts": [ + "localhost" + ], + "Port": "5672", + "HttpApiPort": "15672", + "User": "guest", + "Password": "guest", + "VirtualHost": "EndpointSupportsRequestReplyCommunicationPattern", + "ApplicationName": "EndpointSupportsRequestReplyCommunicationPattern", + "ConsumerPrefetchCount": 100, + "QueueMaxLengthBytes": 1048576, + "ConsumerPriority": 0 + } + }, + "InMemoryIntegrationTransport": { } + }, + "Endpoints": { + "Endpoint1": { + "GenericEndpointSettings": { + "RpcRequestSecondsTimeout": 60 + } + } + } +} \ No newline at end of file diff --git a/tests/Tests/GenericHost.Test/Settings/HostBuilderTests/appsettings.json b/tests/Tests/GenericHost.Test/Settings/HostBuilderTests/appsettings.json new file mode 100644 index 00000000..ad863f07 --- /dev/null +++ b/tests/Tests/GenericHost.Test/Settings/HostBuilderTests/appsettings.json @@ -0,0 +1,81 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Debug", + "System": "Information", + "Microsoft": "Information", + "Npgsql": "Information" + } + }, + "Transports": { + "RabbitMqIntegrationTransport": { + "RabbitMqSettings": { + "Hosts": [ + "localhost" + ], + "Port": "5672", + "HttpApiPort": "15672", + "User": "guest", + "Password": "guest", + "VirtualHost": "HostBuilderTests", + "ApplicationName": "HostBuilderTests", + "ConsumerPrefetchCount": 100, + "QueueMaxLengthBytes": 1048576, + "ConsumerPriority": 0 + } + }, + "InMemoryIntegrationTransport": {} + }, + "Endpoints": { + "AuthEndpoint":{ + "Authorization": { + "Issuer": "Test", + "Audience": "Test", + "PrivateKey": "db3OIsj+BXE9NZDy0t8W3TcNekrF+2d/1sFnWG4HnV8TZY30iTOdtVWJG8abWvB1GlOgJuQZdcF2Luqm/hccMw==" + }, + "AuthorizationSettings": { + "TokenExpirationMinutesTimeout": 5 + }, + "GenericEndpointSettings": { + "RpcRequestSecondsTimeout": 60 + }, + "OrmSettings": { + "CommandSecondsTimeout": 60 + }, + "OutboxSettings": { + "OutboxDeliverySecondsInterval": 60 + }, + "SqlDatabaseSettings": { + "ApplicationName": "BuildHostTest", + "Host": "localhost", + "Port": 5432, + "Database": "BuildHostTest", + "IsolationLevel": "ReadCommitted", + "Username": "postgres", + "Password": "Password12!", + "ConnectionPoolSize": 1 + } + }, + "Endpoint1": { + "GenericEndpointSettings": { + "RpcRequestSecondsTimeout": 60 + }, + "OrmSettings": { + "CommandSecondsTimeout": 60 + }, + "OutboxSettings": { + "OutboxDeliverySecondsInterval": 60 + }, + "SqlDatabaseSettings": { + "ApplicationName": "BuildHostTest", + "Host": "localhost", + "Port": 5432, + "Database": "BuildHostTest", + "IsolationLevel": "ReadCommitted", + "Username": "postgres", + "Password": "Password12!", + "ConnectionPoolSize": 1 + } + } + } +} \ No newline at end of file diff --git a/tests/Tests/GenericHost.Test/Settings/JwtAuthToken/appsettings.json b/tests/Tests/GenericHost.Test/Settings/JwtAuthToken/appsettings.json new file mode 100644 index 00000000..4f30d810 --- /dev/null +++ b/tests/Tests/GenericHost.Test/Settings/JwtAuthToken/appsettings.json @@ -0,0 +1,19 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Debug", + "System": "Information", + "Microsoft": "Information", + "Npgsql": "Information" + } + }, + "Endpoints": { + "AuthEndpoint":{ + "Authorization": { + "Issuer": "Test", + "Audience": "Test", + "PrivateKey": "db3OIsj+BXE9NZDy0t8W3TcNekrF+2d/1sFnWG4HnV8TZY30iTOdtVWJG8abWvB1GlOgJuQZdcF2Luqm/hccMw==" + } + } + } +} \ No newline at end of file diff --git a/tests/Tests/GenericHost.Test/Settings/LinqToSqlTest/appsettings.json b/tests/Tests/GenericHost.Test/Settings/LinqToSqlTest/appsettings.json new file mode 100644 index 00000000..600e5c27 --- /dev/null +++ b/tests/Tests/GenericHost.Test/Settings/LinqToSqlTest/appsettings.json @@ -0,0 +1,36 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Debug", + "System": "Information", + "Microsoft": "Information", + "Npgsql": "Information" + } + }, + "Transports": { + "InMemoryIntegrationTransport": { } + }, + "Endpoints": { + "Endpoint1": { + "GenericEndpointSettings": { + "RpcRequestSecondsTimeout": 60 + }, + "OrmSettings": { + "CommandSecondsTimeout": 10 + }, + "OutboxSettings": { + "OutboxDeliverySecondsInterval": 60 + }, + "SqlDatabaseSettings": { + "ApplicationName": "LinqToSqlTest", + "Host": "localhost", + "Port": 5432, + "Database": "LinqToSqlTest", + "IsolationLevel": "ReadCommitted", + "Username": "postgres", + "Password": "Password12!", + "ConnectionPoolSize": 1 + } + } + } +} \ No newline at end of file diff --git a/tests/Tests/GenericHost.Test/Settings/MessagingRequiresAuthorization/appsettings.json b/tests/Tests/GenericHost.Test/Settings/MessagingRequiresAuthorization/appsettings.json new file mode 100644 index 00000000..9ec3d3ed --- /dev/null +++ b/tests/Tests/GenericHost.Test/Settings/MessagingRequiresAuthorization/appsettings.json @@ -0,0 +1,60 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Debug", + "System": "Information", + "Microsoft": "Information", + "Npgsql": "Information" + } + }, + "Transports": { + "RabbitMqIntegrationTransport": { + "RabbitMqSettings": { + "Hosts": [ + "localhost" + ], + "Port": "5672", + "HttpApiPort": "15672", + "User": "guest", + "Password": "guest", + "VirtualHost": "MessagingRequiresAuthorization", + "ApplicationName": "MessagingRequiresAuthorization", + "ConsumerPrefetchCount": 100, + "QueueMaxLengthBytes": 1048576, + "ConsumerPriority": 0 + } + }, + "InMemoryIntegrationTransport": { } + }, + "Endpoints": { + "AuthEndpoint": { + "Authorization": { + "Issuer": "Test", + "Audience": "Test", + "PrivateKey": "db3OIsj+BXE9NZDy0t8W3TcNekrF+2d/1sFnWG4HnV8TZY30iTOdtVWJG8abWvB1GlOgJuQZdcF2Luqm/hccMw==" + }, + "AuthorizationSettings": { + "TokenExpirationMinutesTimeout": 5 + }, + "GenericEndpointSettings": { + "RpcRequestSecondsTimeout": 60 + }, + "OrmSettings": { + "CommandSecondsTimeout": 60 + }, + "OutboxSettings": { + "OutboxDeliverySecondsInterval": 60 + }, + "SqlDatabaseSettings": { + "ApplicationName": "MessagingRequiresAuthorization", + "Host": "localhost", + "Port": 5432, + "Database": "MessagingRequiresAuthorization", + "IsolationLevel": "ReadCommitted", + "Username": "postgres", + "Password": "Password12!", + "ConnectionPoolSize": 1 + } + } + } +} \ No newline at end of file diff --git a/tests/Tests/GenericHost.Test/Settings/OnlyCommandsCanIntroduceChanges/appsettings.json b/tests/Tests/GenericHost.Test/Settings/OnlyCommandsCanIntroduceChanges/appsettings.json new file mode 100644 index 00000000..c32beeae --- /dev/null +++ b/tests/Tests/GenericHost.Test/Settings/OnlyCommandsCanIntroduceChanges/appsettings.json @@ -0,0 +1,52 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Debug", + "System": "Information", + "Microsoft": "Information", + "Npgsql": "Information" + } + }, + "Transports": { + "RabbitMqIntegrationTransport": { + "RabbitMqSettings": { + "Hosts": [ + "localhost" + ], + "Port": "5672", + "HttpApiPort": "15672", + "User": "guest", + "Password": "guest", + "VirtualHost": "OnlyCommandsCanIntroduceChanges", + "ApplicationName": "OnlyCommandsCanIntroduceChanges", + "ConsumerPrefetchCount": 100, + "QueueMaxLengthBytes": 1048576, + "ConsumerPriority": 0 + } + }, + "InMemoryIntegrationTransport": { } + }, + "Endpoints":{ + "Endpoint1": { + "GenericEndpointSettings": { + "RpcRequestSecondsTimeout": 60 + }, + "OrmSettings": { + "CommandSecondsTimeout": 60 + }, + "OutboxSettings": { + "OutboxDeliverySecondsInterval": 60 + }, + "SqlDatabaseSettings": { + "ApplicationName": "OnlyCommandsCanIntroduceChanges", + "Host": "localhost", + "Port": 5432, + "Database": "OnlyCommandsCanIntroduceChanges", + "IsolationLevel": "ReadCommitted", + "Username": "postgres", + "Password": "Password12!", + "ConnectionPoolSize": 1 + } + } + } +} \ No newline at end of file diff --git a/tests/Tests/GenericHost.Test/Settings/OrmAppliesCascadeDeleteStrategy/appsettings.json b/tests/Tests/GenericHost.Test/Settings/OrmAppliesCascadeDeleteStrategy/appsettings.json new file mode 100644 index 00000000..0d564a1c --- /dev/null +++ b/tests/Tests/GenericHost.Test/Settings/OrmAppliesCascadeDeleteStrategy/appsettings.json @@ -0,0 +1,52 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Debug", + "System": "Information", + "Microsoft": "Information", + "Npgsql": "Information" + } + }, + "Transports": { + "RabbitMqIntegrationTransport": { + "RabbitMqSettings": { + "Hosts": [ + "localhost" + ], + "Port": "5672", + "HttpApiPort": "15672", + "User": "guest", + "Password": "guest", + "VirtualHost": "OrmAppliesCascadeDeleteStrategy", + "ApplicationName": "OrmAppliesCascadeDeleteStrategy", + "ConsumerPrefetchCount": 100, + "QueueMaxLengthBytes": 1048576, + "ConsumerPriority": 0 + } + }, + "InMemoryIntegrationTransport": { } + }, + "Endpoints": { + "Endpoint1": { + "GenericEndpointSettings": { + "RpcRequestSecondsTimeout": 60 + }, + "OrmSettings": { + "CommandSecondsTimeout": 60 + }, + "OutboxSettings": { + "OutboxDeliverySecondsInterval": 60 + }, + "SqlDatabaseSettings": { + "ApplicationName": "OrmAppliesCascadeDeleteStrategy", + "Host": "localhost", + "Port": 5432, + "Database": "OrmAppliesCascadeDeleteStrategy", + "IsolationLevel": "ReadCommitted", + "Username": "postgres", + "Password": "Password12!", + "ConnectionPoolSize": 1 + } + } + } +} \ No newline at end of file diff --git a/tests/Tests/GenericHost.Test/Settings/OrmTracksEntityChanges/appsettings.json b/tests/Tests/GenericHost.Test/Settings/OrmTracksEntityChanges/appsettings.json new file mode 100644 index 00000000..21a27041 --- /dev/null +++ b/tests/Tests/GenericHost.Test/Settings/OrmTracksEntityChanges/appsettings.json @@ -0,0 +1,52 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Debug", + "System": "Information", + "Microsoft": "Information", + "Npgsql": "Information" + } + }, + "Transports": { + "RabbitMqIntegrationTransport": { + "RabbitMqSettings": { + "Hosts": [ + "localhost" + ], + "Port": "5672", + "HttpApiPort": "15672", + "User": "guest", + "Password": "guest", + "VirtualHost": "OrmTracksEntityChanges", + "ApplicationName": "OrmTracksEntityChanges", + "ConsumerPrefetchCount": 100, + "QueueMaxLengthBytes": 1048576, + "ConsumerPriority": 0 + } + }, + "InMemoryIntegrationTransport": { } + }, + "Endpoints":{ + "Endpoint1": { + "GenericEndpointSettings": { + "RpcRequestSecondsTimeout": 60 + }, + "OrmSettings": { + "CommandSecondsTimeout": 60 + }, + "OutboxSettings": { + "OutboxDeliverySecondsInterval": 60 + }, + "SqlDatabaseSettings": { + "ApplicationName": "OrmTracksEntityChanges", + "Host": "localhost", + "Port": 5432, + "Database": "OrmTracksEntityChanges", + "IsolationLevel": "ReadCommitted", + "Username": "postgres", + "Password": "Password12!", + "ConnectionPoolSize": 1 + } + } + } +} \ No newline at end of file diff --git a/tests/Tests/GenericHost.Test/Settings/OutboxDeliversMessagesInBackground/appsettings.json b/tests/Tests/GenericHost.Test/Settings/OutboxDeliversMessagesInBackground/appsettings.json new file mode 100644 index 00000000..802d5cf9 --- /dev/null +++ b/tests/Tests/GenericHost.Test/Settings/OutboxDeliversMessagesInBackground/appsettings.json @@ -0,0 +1,52 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Debug", + "System": "Information", + "Microsoft": "Information", + "Npgsql": "Information" + } + }, + "Transports": { + "RabbitMqIntegrationTransport": { + "RabbitMqSettings": { + "Hosts": [ + "localhost" + ], + "Port": "5672", + "HttpApiPort": "15672", + "User": "guest", + "Password": "guest", + "VirtualHost": "OutboxDeliversMessagesInBackground", + "ApplicationName": "OutboxDeliversMessagesInBackground", + "ConsumerPrefetchCount": 100, + "QueueMaxLengthBytes": 1048576, + "ConsumerPriority": 0 + } + }, + "InMemoryIntegrationTransport": { } + }, + "Endpoints": { + "Endpoint1": { + "GenericEndpointSettings": { + "RpcRequestSecondsTimeout": 60 + }, + "OrmSettings": { + "CommandSecondsTimeout": 60 + }, + "OutboxSettings": { + "OutboxDeliverySecondsInterval": 1 + }, + "SqlDatabaseSettings": { + "ApplicationName": "OutboxDeliversMessagesInBackground", + "Host": "localhost", + "Port": 5432, + "Database": "OutboxDeliversMessagesInBackground", + "IsolationLevel": "ReadCommitted", + "Username": "postgres", + "Password": "Password12!", + "ConnectionPoolSize": 1 + } + } + } +} \ No newline at end of file diff --git a/tests/Tests/GenericHost.Test/StartupActions/CreateOrGetExistedPostgreSqlDatabaseHostedServiceStartupAction.cs b/tests/Tests/GenericHost.Test/StartupActions/CreateOrGetExistedPostgreSqlDatabaseHostedServiceStartupAction.cs new file mode 100644 index 00000000..7503c183 --- /dev/null +++ b/tests/Tests/GenericHost.Test/StartupActions/CreateOrGetExistedPostgreSqlDatabaseHostedServiceStartupAction.cs @@ -0,0 +1,121 @@ +namespace SpaceEngineers.Core.GenericHost.Test.StartupActions +{ + using System; + using System.Diagnostics.CodeAnalysis; + using System.Threading; + using System.Threading.Tasks; + using AutoRegistration.Api.Abstractions; + using AutoRegistration.Api.Attributes; + using AutoRegistration.Api.Enumerations; + using Basics; + using Basics.Attributes; + using CompositionRoot; + using CrossCuttingConcerns.Settings; + using DataAccess.Orm.Sql.Connection; + using DataAccess.Orm.Sql.Settings; + using DataAccess.Orm.Sql.Translation; + using GenericEndpoint.DataAccess.Sql.Host.StartupActions; + using Npgsql; + + [Component(EnLifestyle.Singleton)] + [Before(typeof(UpgradeDatabaseHostedServiceStartupAction))] + internal class CreateOrGetExistedPostgreSqlDatabaseHostedServiceStartupAction : IHostedServiceStartupAction, + ICollectionResolvable, + ICollectionResolvable, + IResolvable + { + private const string CommandText = @"create extension if not exists dblink; + +create or replace function CreateOrGetExistedDatabase() returns boolean as +$BODY$ + declare isDatabaseExists boolean default false; + + begin + select exists(select * from pg_catalog.pg_database where datname = '{0}') into isDatabaseExists; + + if + isDatabaseExists + then + raise notice 'database already exists'; + else + perform dblink_connect('host=localhost user=' || '{1}' || ' password=' || '{2}' || ' dbname=' || current_database()); + perform dblink_exec('create database ""{0}""'); + perform dblink_exec('grant all privileges on database ""{0}"" to ""{1}""'); + end if; + + return not isDatabaseExists; + end +$BODY$ +language plpgsql; + +select CreateOrGetExistedDatabase();"; + + private readonly SqlDatabaseSettings _sqlDatabaseSettings; + private readonly IDependencyContainer _dependencyContainer; + private readonly IDatabaseConnectionProvider _connectionProvider; + + public CreateOrGetExistedPostgreSqlDatabaseHostedServiceStartupAction( + ISettingsProvider sqlDatabaseSettingsProvider, + IDependencyContainer dependencyContainer, + IDatabaseConnectionProvider connectionProvider) + { + _sqlDatabaseSettings = sqlDatabaseSettingsProvider.Get(); + + _dependencyContainer = dependencyContainer; + _connectionProvider = connectionProvider; + } + + [SuppressMessage("Analysis", "CA2000", Justification = "IDbConnection will be disposed in outer scope by client")] + public async Task Run(CancellationToken token) + { + var command = new SqlCommand( + CommandText.Format(_sqlDatabaseSettings.Database, _sqlDatabaseSettings.Username, _sqlDatabaseSettings.Password), + Array.Empty()); + + var connectionStringBuilder = new NpgsqlConnectionStringBuilder + { + Host = _sqlDatabaseSettings.Host, + Port = _sqlDatabaseSettings.Port, + Database = "postgres", + Username = _sqlDatabaseSettings.Username, + Password = _sqlDatabaseSettings.Password + }; + + var npgSqlConnection = new NpgsqlConnection(connectionStringBuilder.ConnectionString); + + try + { + await npgSqlConnection.OpenAsync(token).ConfigureAwait(false); + + _ = await _connectionProvider + .ExecuteScalar(npgSqlConnection, command, token) + .ConfigureAwait(false); + } + finally + { + npgSqlConnection.Dispose(); + } + + NpgsqlConnection.ClearPool(npgSqlConnection); + + while (true) + { + var doesDatabaseExist = await _dependencyContainer + .Resolve() + .DoesDatabaseExist(token) + .ConfigureAwait(false); + + if (!doesDatabaseExist) + { + await Task + .Delay(TimeSpan.FromMilliseconds(100), token) + .ConfigureAwait(false); + } + else + { + break; + } + } + } + } +} \ No newline at end of file diff --git a/tests/Tests/GenericHost.Test/StartupActions/PurgeRabbitMqQueuesHostedServiceStartupAction.cs b/tests/Tests/GenericHost.Test/StartupActions/PurgeRabbitMqQueuesHostedServiceStartupAction.cs new file mode 100644 index 00000000..40f7aeac --- /dev/null +++ b/tests/Tests/GenericHost.Test/StartupActions/PurgeRabbitMqQueuesHostedServiceStartupAction.cs @@ -0,0 +1,44 @@ +namespace SpaceEngineers.Core.GenericHost.Test.StartupActions +{ + using System.Threading; + using System.Threading.Tasks; + using AutoRegistration.Api.Abstractions; + using AutoRegistration.Api.Attributes; + using Basics.Attributes; + using CrossCuttingConcerns.Json; + using CrossCuttingConcerns.Settings; + using IntegrationTransport.RabbitMQ.Extensions; + using IntegrationTransport.RabbitMQ.Settings; + using Microsoft.Extensions.Logging; + using SpaceEngineers.Core.GenericEndpoint.Host.StartupActions; + + [ManuallyRegisteredComponent("Hosting dependency that implicitly participates in composition")] + [Before(typeof(GenericEndpointHostedServiceStartupAction))] + internal class PurgeRabbitMqQueuesHostedServiceStartupAction : IHostedServiceStartupAction, + ICollectionResolvable, + ICollectionResolvable, + IResolvable + { + private readonly RabbitMqSettings _rabbitMqSettings; + private readonly IJsonSerializer _jsonSerializer; + private readonly ILogger _logger; + + public PurgeRabbitMqQueuesHostedServiceStartupAction( + ISettingsProvider rabbitMqSettingsProvider, + IJsonSerializer jsonSerializer, + ILogger logger) + { + _rabbitMqSettings = rabbitMqSettingsProvider.Get(); + + _jsonSerializer = jsonSerializer; + _logger = logger; + } + + public async Task Run(CancellationToken token) + { + await _rabbitMqSettings + .PurgeMessages(_jsonSerializer, _logger, token) + .ConfigureAwait(false); + } + } +} \ No newline at end of file diff --git a/tests/Tests/GenericHost.Test/StartupActions/RecreatePostgreSqlDatabaseHostedServiceStartupAction.cs b/tests/Tests/GenericHost.Test/StartupActions/RecreatePostgreSqlDatabaseHostedServiceStartupAction.cs new file mode 100644 index 00000000..21b5521c --- /dev/null +++ b/tests/Tests/GenericHost.Test/StartupActions/RecreatePostgreSqlDatabaseHostedServiceStartupAction.cs @@ -0,0 +1,101 @@ +namespace SpaceEngineers.Core.GenericHost.Test.StartupActions +{ + using System; + using System.Diagnostics.CodeAnalysis; + using System.Threading; + using System.Threading.Tasks; + using AutoRegistration.Api.Abstractions; + using AutoRegistration.Api.Attributes; + using AutoRegistration.Api.Enumerations; + using Basics; + using Basics.Attributes; + using CompositionRoot; + using CrossCuttingConcerns.Settings; + using DataAccess.Orm.Sql.Connection; + using DataAccess.Orm.Sql.Settings; + using DataAccess.Orm.Sql.Translation; + using GenericEndpoint.DataAccess.Sql.Host.StartupActions; + using Npgsql; + + [Component(EnLifestyle.Singleton)] + [Before(typeof(UpgradeDatabaseHostedServiceStartupAction))] + internal class RecreatePostgreSqlDatabaseHostedServiceStartupAction : IHostedServiceStartupAction, + ICollectionResolvable, + ICollectionResolvable, + IResolvable + { + private const string CommandText = @"create extension if not exists dblink; + +drop database if exists ""{0}"" with (FORCE); +create database ""{0}""; +grant all privileges on database ""{0}"" to ""{1}"";"; + + private readonly SqlDatabaseSettings _sqlDatabaseSettings; + private readonly IDependencyContainer _dependencyContainer; + private readonly IDatabaseConnectionProvider _connectionProvider; + + public RecreatePostgreSqlDatabaseHostedServiceStartupAction( + ISettingsProvider sqlDatabaseSettingsProvider, + IDependencyContainer dependencyContainer, + IDatabaseConnectionProvider connectionProvider) + { + _sqlDatabaseSettings = sqlDatabaseSettingsProvider.Get(); + + _dependencyContainer = dependencyContainer; + _connectionProvider = connectionProvider; + } + + [SuppressMessage("Analysis", "CA2000", Justification = "IDbConnection will be disposed in outer scope by client")] + public async Task Run(CancellationToken token) + { + var command = new SqlCommand( + CommandText.Format(_sqlDatabaseSettings.Database, _sqlDatabaseSettings.Username), + Array.Empty()); + + var connectionStringBuilder = new NpgsqlConnectionStringBuilder + { + Host = _sqlDatabaseSettings.Host, + Port = _sqlDatabaseSettings.Port, + Database = "postgres", + Username = _sqlDatabaseSettings.Username, + Password = _sqlDatabaseSettings.Password + }; + + var npgSqlConnection = new NpgsqlConnection(connectionStringBuilder.ConnectionString); + + try + { + await npgSqlConnection.OpenAsync(token).ConfigureAwait(false); + + _ = await _connectionProvider + .Execute(npgSqlConnection, command, token) + .ConfigureAwait(false); + } + finally + { + npgSqlConnection.Dispose(); + } + + NpgsqlConnection.ClearPool(npgSqlConnection); + + while (true) + { + var doesDatabaseExist = await _dependencyContainer + .Resolve() + .DoesDatabaseExist(token) + .ConfigureAwait(false); + + if (!doesDatabaseExist) + { + await Task + .Delay(TimeSpan.FromMilliseconds(100), token) + .ConfigureAwait(false); + } + else + { + break; + } + } + } + } +} \ No newline at end of file diff --git a/tests/Tests/GenericHost.Test/TestExtensionsTests.cs b/tests/Tests/GenericHost.Test/TestExtensionsTests.cs new file mode 100644 index 00000000..47bd18df --- /dev/null +++ b/tests/Tests/GenericHost.Test/TestExtensionsTests.cs @@ -0,0 +1,106 @@ +namespace SpaceEngineers.Core.GenericHost.Test +{ + using System; + using GenericEndpoint.Contract.Abstractions; + using GenericEndpoint.TestExtensions; + using GenericEndpoint.TestExtensions.Internals; + using MessageHandlers; + using Messages; + using SpaceEngineers.Core.Test.Api; + using SpaceEngineers.Core.Test.Api.ClassFixtures; + using Xunit; + using Xunit.Abstractions; + using EventHandler = MessageHandlers.EventHandler; + + /// + /// GenericEndpoint.TestExtensions test + /// + public class TestExtensionsTests : TestBase + { + /// .cctor + /// ITestOutputHelper + /// TestFixture + public TestExtensionsTests(ITestOutputHelper output, TestFixture fixture) + : base(output, fixture) + { + } + + [Fact] + internal void Assert_message_handler_test_extensions() + { + { + var context = new TestIntegrationContext(); + + new CommandHandler(TestIdentity.Endpoint10, context) + .OnMessage(new Command(42)) + .Publishes(invoked => + invoked.EndpointIdentity == TestIdentity.Endpoint10 + && invoked.HandlerType == typeof(CommandHandler)) + .DoesNotThrow() + .Invoke(context); + } + + { + var context = new TestIntegrationContext(); + + new ThrowingCommandHandler() + .OnMessage(new Command(42)) + .ProducesNothing() + .Throws(ex => ex.Message == "42") + .Invoke(context); + } + + { + var context = new TestIntegrationContext(); + + new EventHandler(TestIdentity.Endpoint20, context) + .OnMessage(new Event(42)) + .Publishes(invoked => + invoked.EndpointIdentity == TestIdentity.Endpoint20 + && invoked.HandlerType == typeof(EventHandler)) + .DoesNotThrow() + .Invoke(context); + } + + { + var context = new TestIntegrationContext(); + + new OddReplyRequestHandler(context) + .OnMessage(new Request(42)) + .ProducesNothing() + .DoesNotThrow() + .Invoke(context); + } + + { + var context = new TestIntegrationContext(); + + new OddReplyRequestHandler(context) + .OnMessage(new Request(43)) + .DoesNotSend() + .DoesNotDelay() + .DoesNotPublish() + .DoesNotRequest() + .Replies(reply => reply.Id == 43) + .DoesNotThrow() + .Invoke(context); + } + + { + var context = new TestIntegrationContext(); + + new SendDelayedCommandEventHandler(context) + .OnMessage(new Event(42)) + .DoesNotSend() + .Delays((command, dateTime) => + command.Id == 42 && + (int)Math.Round((dateTime.ToUniversalTime() - DateTime.UtcNow).TotalDays, MidpointRounding.AwayFromZero) == 42) + .DoesNotPublish() + .DoesNotRequest() + .DoesNotReply() + .DoesNotThrow() + .Invoke(context); + } + } + } +} \ No newline at end of file diff --git a/tests/Tests/Modules.Test/CliArgumentsParserTest.cs b/tests/Tests/Modules.Test/CliArgumentsParserTest.cs new file mode 100644 index 00000000..cdc2351c --- /dev/null +++ b/tests/Tests/Modules.Test/CliArgumentsParserTest.cs @@ -0,0 +1,420 @@ +namespace SpaceEngineers.Core.Modules.Test +{ + using System; + using System.Diagnostics.CodeAnalysis; + using System.Reflection; + using Basics; + using CliArgumentsParser; + using CompositionRoot; + using Core.Test.Api; + using Core.Test.Api.ClassFixtures; + using Xunit; + using Xunit.Abstractions; + + /// + /// CliArgumentsParser class tests + /// + public class CliArgumentsParserTest : TestBase + { + /// .ctor + /// ITestOutputHelper + /// TestFixture + public CliArgumentsParserTest(ITestOutputHelper output, TestFixture fixture) + : base(output, fixture) + { + var assemblies = new[] + { + AssembliesExtensions.FindRequiredAssembly(AssembliesExtensions.BuildName(nameof(SpaceEngineers), nameof(Core), nameof(CliArgumentsParser))) + }; + + var options = new DependencyContainerOptions() + .WithPluginAssemblies(assemblies); + + DependencyContainer = fixture.DependencyContainer(options); + } + + private IDependencyContainer DependencyContainer { get; } + + #pragma warning disable xUnit2000 // Constants and literals should be the expected argument + + [Fact] + internal void Test1() + { + var parser = DependencyContainer.Resolve(); + + var args = new[] + { + "-bool", + "-nullablebool", + "-testenum=value1", + "-nullabletestenum=Value2" + }; + + var arguments = parser.Parse(args); + + Output.WriteLine(arguments.ToString()); + + Assert.Equal(arguments.Bool, true); + Assert.Equal(arguments.NullableBool, true); + Assert.Equal(arguments.TestEnum, TestEnum.Value1); + Assert.Equal(arguments.NullableTestEnum, TestEnum.Value2); + Assert.Equal(arguments.TestFlagsEnum, TestFlagsEnum.Default); + Assert.Equal(arguments.NullableTestFlagsEnum, null); + Assert.Equal(arguments.String, null); + } + + [Fact] + internal void Test2() + { + var parser = DependencyContainer.Resolve(); + + var args = new[] + { + "-bool=true", + "-nullablebool=true", + "-testenum=Value1", + "-nullabletestenum=value2" + }; + + var arguments = parser.Parse(args); + + Output.WriteLine(arguments.ToString()); + + Assert.Equal(arguments.Bool, true); + Assert.Equal(arguments.NullableBool, true); + Assert.Equal(arguments.TestEnum, TestEnum.Value1); + Assert.Equal(arguments.NullableTestEnum, TestEnum.Value2); + Assert.Equal(arguments.TestFlagsEnum, TestFlagsEnum.Default); + Assert.Equal(arguments.NullableTestFlagsEnum, null); + Assert.Equal(arguments.String, null); + } + + [Fact] + internal void Test4() + { + var parser = DependencyContainer.Resolve(); + + var args = new[] + { + "-string" + }; + + var arguments = parser.Parse(args); + + Output.WriteLine(arguments.ToString()); + + Assert.Equal(arguments.Bool, false); + Assert.Equal(arguments.NullableBool, null); + Assert.Equal(arguments.TestEnum, TestEnum.Default); + Assert.Equal(arguments.NullableTestEnum, null); + Assert.Equal(arguments.TestFlagsEnum, TestFlagsEnum.Default); + Assert.Equal(arguments.NullableTestFlagsEnum, null); + Assert.Equal(arguments.String, null); + } + + [Fact] + internal void Test5() + { + var parser = DependencyContainer.Resolve(); + + var args = new[] + { + "-string=qwerty" + }; + + var arguments = parser.Parse(args); + + Assert.Equal(arguments.Bool, false); + Assert.Equal(arguments.NullableBool, null); + Assert.Equal(arguments.TestEnum, TestEnum.Default); + Assert.Equal(arguments.NullableTestEnum, null); + Assert.Equal(arguments.TestFlagsEnum, TestFlagsEnum.Default); + Assert.Equal(arguments.NullableTestFlagsEnum, null); + Assert.Equal(arguments.String, "qwerty"); + } + + [Fact] + internal void Test6() + { + var parser = DependencyContainer.Resolve(); + + var args = new[] + { + "-string=\"qwerty qwerty\"" + }; + + var arguments = parser.Parse(args); + + Output.WriteLine(arguments.ToString()); + + Assert.Equal(arguments.Bool, false); + Assert.Equal(arguments.NullableBool, null); + Assert.Equal(arguments.TestEnum, TestEnum.Default); + Assert.Equal(arguments.NullableTestEnum, null); + Assert.Equal(arguments.TestFlagsEnum, TestFlagsEnum.Default); + Assert.Equal(arguments.NullableTestFlagsEnum, null); + Assert.Equal(arguments.String, "qwerty qwerty"); + } + + [Fact] + internal void Test7() + { + var parser = DependencyContainer.Resolve(); + + var args = new[] + { + "-string=\"https://www.google.com\"" + }; + + var arguments = parser.Parse(args); + + Output.WriteLine(arguments.ToString()); + + Assert.Equal(arguments.Bool, false); + Assert.Equal(arguments.NullableBool, null); + Assert.Equal(arguments.TestEnum, TestEnum.Default); + Assert.Equal(arguments.NullableTestEnum, null); + Assert.Equal(arguments.TestFlagsEnum, TestFlagsEnum.Default); + Assert.Equal(arguments.NullableTestFlagsEnum, null); + Assert.Equal(arguments.String, "https://www.google.com"); + } + + [Fact] + internal void Test8() + { + var parser = DependencyContainer.Resolve(); + + var args = new[] + { + "-string=https://www.google.com" + }; + + var arguments = parser.Parse(args); + + Output.WriteLine(arguments.ToString()); + + Assert.Equal(arguments.Bool, false); + Assert.Equal(arguments.NullableBool, null); + Assert.Equal(arguments.TestEnum, TestEnum.Default); + Assert.Equal(arguments.NullableTestEnum, null); + Assert.Equal(arguments.TestFlagsEnum, TestFlagsEnum.Default); + Assert.Equal(arguments.NullableTestFlagsEnum, null); + Assert.Equal(arguments.String, "https://www.google.com"); + } + + [Fact] + internal void Test9() + { + var parser = DependencyContainer.Resolve(); + + var args = new[] + { + "-string='https://www.yandex.ru;https://www.google.com'" + }; + + var arguments = parser.Parse(args); + + Output.WriteLine(arguments.ToString()); + + Assert.Equal(arguments.Bool, false); + Assert.Equal(arguments.NullableBool, null); + Assert.Equal(arguments.TestEnum, TestEnum.Default); + Assert.Equal(arguments.NullableTestEnum, null); + Assert.Equal(arguments.TestFlagsEnum, TestFlagsEnum.Default); + Assert.Equal(arguments.NullableTestFlagsEnum, null); + Assert.Equal(arguments.String, "https://www.yandex.ru;https://www.google.com"); + } + + [Fact] + internal void Test10() + { + var parser = DependencyContainer.Resolve(); + + var args = new[] + { + "-TestFlagsEnum=Value1", + "-NullableTestFlagsEnum=value2" + }; + + var arguments = parser.Parse(args); + + Output.WriteLine(arguments.ToString()); + + Assert.Equal(arguments.Bool, false); + Assert.Equal(arguments.NullableBool, null); + Assert.Equal(arguments.TestEnum, TestEnum.Default); + Assert.Equal(arguments.NullableTestEnum, null); + Assert.Equal(arguments.TestFlagsEnum, TestFlagsEnum.Value1); + Assert.Equal(arguments.NullableTestFlagsEnum, TestFlagsEnum.Value2); + Assert.Equal(arguments.String, null); + } + + [Fact] + internal void Test11() + { + var parser = DependencyContainer.Resolve(); + + var args = new[] + { + "-TestFlagsEnum=value1", + "-NullableTestFlagsEnum=value2;VaLuE1" + }; + + var arguments = parser.Parse(args); + + Output.WriteLine(arguments.ToString()); + + Assert.Equal(arguments.Bool, false); + Assert.Equal(arguments.NullableBool, null); + Assert.Equal(arguments.TestEnum, TestEnum.Default); + Assert.Equal(arguments.NullableTestEnum, null); + Assert.Equal(arguments.TestFlagsEnum, TestFlagsEnum.Value1); + Assert.Equal(arguments.NullableTestFlagsEnum, TestFlagsEnum.Value1 | TestFlagsEnum.Value2); + Assert.Equal(arguments.String, null); + } + + [Fact] + internal void Test12() + { + var args = new[] + { + "-bool", + "-bool" + }; + + var ex = Assert.Throws(() => DependencyContainer.Resolve().Parse(args)); + + Assert.Contains("'bool' already added", ex.Message, StringComparison.Ordinal); + } + + [Fact] + internal void Test13() + { + var parser = DependencyContainer.Resolve(); + + var args = new[] + { + "-nullablebool", + "-TestFlagsEnum=value3", + "-bool" + }; + + var ex = Assert.Throws(() => parser.Parse(args)); + Assert.Contains("Value 'value3' is not recognized", ex.Message, StringComparison.Ordinal); + } + + [Fact] + internal void Test14() + { + var parser = DependencyContainer.Resolve(); + + var args = new[] + { + "-nullablebool", + "-NullableTestFlagsEnum=\"value2 ; VaLuE1\"", + "-bool" + }; + + var arguments = parser.Parse(args); + + Output.WriteLine(arguments.ToString()); + + Assert.Equal(arguments.Bool, true); + Assert.Equal(arguments.NullableBool, true); + Assert.Equal(arguments.TestEnum, TestEnum.Default); + Assert.Equal(arguments.NullableTestEnum, null); + Assert.Equal(arguments.TestFlagsEnum, TestFlagsEnum.Default); + Assert.Equal(arguments.NullableTestFlagsEnum, TestFlagsEnum.Value1 | TestFlagsEnum.Value2); + Assert.Equal(arguments.String, null); + } + + [Fact] + internal void Test15() + { + var parser = DependencyContainer.Resolve(); + + var args = new[] + { + "-nullablebool", + "-NullableTestFlagsEnum=value2 ; VaLuE1", + "-bool" + }; + + var arguments = parser.Parse(args); + + Output.WriteLine(arguments.ToString()); + + Assert.Equal(arguments.Bool, true); + Assert.Equal(arguments.NullableBool, true); + Assert.Equal(arguments.TestEnum, TestEnum.Default); + Assert.Equal(arguments.NullableTestEnum, null); + Assert.Equal(arguments.TestFlagsEnum, TestFlagsEnum.Default); + Assert.Equal(arguments.NullableTestFlagsEnum, TestFlagsEnum.Value1 | TestFlagsEnum.Value2); + Assert.Equal(arguments.String, null); + } + + [Fact] + internal void Test16() + { + var parser = DependencyContainer.Resolve(); + + var args = new[] + { + "-nullablebool", + "-NullableTestFlagsEnum=value2;value3", + "-bool" + }; + + var ex = Assert.Throws(() => parser.Parse(args)); + Assert.Contains("Values 'value3' is not recognized", ex.Message, StringComparison.Ordinal); + } + + #pragma warning restore xUnit2000 // Constants and literals should be the expected argument + + [SuppressMessage("Analysis", "SA1201", Justification = "For test reasons")] + private enum TestEnum + { + Default, + Value1, + Value2 + } + + [Flags] + private enum TestFlagsEnum + { + Default = 0, + Value1 = 1 << 0, + Value2 = 1 << 1 + } + + private class TestPoco + { + /// Bool + public bool Bool { get; set; } + + /// NullableBool + public bool? NullableBool { get; set; } + + /// TestEnum + public TestEnum TestEnum { get; set; } + + /// NullableTestEnum + public TestEnum? NullableTestEnum { get; set; } + + /// TestFlagsEnum + public TestFlagsEnum TestFlagsEnum { get; set; } + + /// NullableTestFlagsEnum + public TestFlagsEnum? NullableTestFlagsEnum { get; set; } + + /// String + public string? String { get; set; } + + /// + public override string ToString() + { + return this.Dump(BindingFlags.Public | BindingFlags.Instance | BindingFlags.GetProperty | BindingFlags.SetProperty); + } + } + } +} \ No newline at end of file diff --git a/tests/Tests/Modules.Test/CompositionInfoExtractorTest.cs b/tests/Tests/Modules.Test/CompositionInfoExtractorTest.cs new file mode 100644 index 00000000..8b4b73db --- /dev/null +++ b/tests/Tests/Modules.Test/CompositionInfoExtractorTest.cs @@ -0,0 +1,50 @@ +namespace SpaceEngineers.Core.Modules.Test +{ + using System; + using System.Linq; + using CompositionRoot; + using CompositionRoot.CompositionInfo; + using Core.Test.Api; + using Core.Test.Api.ClassFixtures; + using Xunit; + using Xunit.Abstractions; + + /// + /// CompositionInfoExtractor class tests + /// + public class CompositionInfoExtractorTest : TestBase + { + /// .ctor + /// ITestOutputHelper + /// TestFixture + public CompositionInfoExtractorTest(ITestOutputHelper output, TestFixture fixture) + : base(output, fixture) + { + var options = new DependencyContainerOptions(); + + DependencyContainer = fixture.DependencyContainer(options); + } + + private IDependencyContainer DependencyContainer { get; } + + [Theory] + [InlineData(false)] + [InlineData(true)] + internal void CompositionInfoTest(bool mode) + { + using (DependencyContainer.OpenScope()) + { + var compositionInfo = DependencyContainer + .Resolve() + .GetCompositionInfo(mode) + .ToArray(); + + Output.WriteLine($"Total: {compositionInfo.Length}{Environment.NewLine}"); + + Output.WriteLine(DependencyContainer + .Resolve>() + .Visualize(compositionInfo)); + } + } + } +} \ No newline at end of file diff --git a/tests/Tests/Modules.Test/CrossCuttingConcernsTest.cs b/tests/Tests/Modules.Test/CrossCuttingConcernsTest.cs new file mode 100644 index 00000000..797a44a9 --- /dev/null +++ b/tests/Tests/Modules.Test/CrossCuttingConcernsTest.cs @@ -0,0 +1,87 @@ +namespace SpaceEngineers.Core.Modules.Test +{ + using System; + using System.Collections.Generic; + using AutoRegistration.Api.Enumerations; + using Basics; + using CompositionRoot; + using Core.Test.Api; + using Core.Test.Api.ClassFixtures; + using CrossCuttingConcerns.ObjectBuilder; + using CrossCuttingConcerns.Settings; + using Xunit; + using Xunit.Abstractions; + + /// + /// CrossCuttingConcerns assembly tests + /// + public class CrossCuttingConcernsTest : TestBase + { + /// .ctor + /// ITestOutputHelper + /// TestFixture + public CrossCuttingConcernsTest(ITestOutputHelper output, TestFixture fixture) + : base(output, fixture) + { + var projectFileDirectory = SolutionExtensions.ProjectFile().Directory + ?? throw new InvalidOperationException("Project directory wasn't found"); + + var settingsDirectory = projectFileDirectory.StepInto("Settings"); + + var assemblies = new[] + { + AssembliesExtensions.FindRequiredAssembly(AssembliesExtensions.BuildName(nameof(SpaceEngineers), nameof(Core), nameof(CrossCuttingConcerns))) + }; + + var options = new DependencyContainerOptions() + .WithPluginAssemblies(assemblies) + .WithManualRegistrations(new SettingsDirectoryProviderManualRegistration(new SettingsDirectoryProvider(settingsDirectory))); + + DependencyContainer = fixture.DependencyContainer(options); + } + + private IDependencyContainer DependencyContainer { get; } + + [Fact] + internal void ObjectBuilderTest() + { + Assert.Equal( + "qwerty", + DependencyContainer.Resolve>().Build(new Dictionary { ["value"] = "qwerty" })); + + Assert.Equal( + EnLifestyle.Scoped, + DependencyContainer.Resolve>().Build(new Dictionary { ["value"] = EnLifestyle.Scoped.ToString() })); + + Assert.Equal( + Guid.Parse("50EA09BF-C8C1-494B-8D35-8B4C86A5A344"), + DependencyContainer.Resolve>().Build(new Dictionary { ["value"] = Guid.Parse("50EA09BF-C8C1-494B-8D35-8B4C86A5A344") })); + + Assert.Equal( + TypeNode.FromType(TypeExtensions.FindType("System.Private.CoreLib System.DateOnly")), + DependencyContainer.Resolve>().Build(new Dictionary { ["value"] = "System.Private.CoreLib System.DateOnly" })); + + Assert.Equal( + TypeExtensions.FindType("System.Private.CoreLib System.DateOnly"), + DependencyContainer.Resolve>().Build(new Dictionary { ["value"] = "System.Private.CoreLib System.DateOnly" })); + + Assert.Equal( + "System.Private.CoreLib System.DateOnly", + DependencyContainer.Resolve>().Build(new Dictionary { ["value"] = TypeExtensions.FindType("System.Private.CoreLib System.DateOnly") })); + + Assert.Equal( + "qwerty", + DependencyContainer.Resolve>().Build(new Dictionary { [nameof(TestClass.Value)] = "qwerty" }).Value); + } + + private class TestClass + { + public TestClass(string value) + { + Value = value; + } + + internal string Value { get; } + } + } +} \ No newline at end of file diff --git a/tests/Tests/Modules.Test/DataExportImportTest.cs b/tests/Tests/Modules.Test/DataExportImportTest.cs new file mode 100644 index 00000000..f866702a --- /dev/null +++ b/tests/Tests/Modules.Test/DataExportImportTest.cs @@ -0,0 +1,192 @@ +namespace SpaceEngineers.Core.Modules.Test +{ + using System; + using System.Collections.Generic; + using System.Data; + using System.Globalization; + using System.Linq; + using AutoRegistration.Api.Attributes; + using AutoRegistration.Api.Enumerations; + using Basics; + using CompositionRoot; + using Core.Test.Api; + using Core.Test.Api.ClassFixtures; + using DataExport.Excel; + using DataImport; + using DataImport.Abstractions; + using DataImport.Excel; + using Xunit; + using Xunit.Abstractions; + + /// + /// DataExport/DataImport assembly tests + /// + public class DataExportImportTest : TestBase + { + /// .ctor + /// ITestOutputHelper + /// TestFixture + public DataExportImportTest(ITestOutputHelper output, TestFixture fixture) + : base(output, fixture) + { + var assemblies = new[] + { + AssembliesExtensions.FindRequiredAssembly(AssembliesExtensions.BuildName(nameof(SpaceEngineers), nameof(Core), nameof(DataExport))), + AssembliesExtensions.FindRequiredAssembly(AssembliesExtensions.BuildName(nameof(SpaceEngineers), nameof(Core), nameof(DataImport))) + }; + + var options = new DependencyContainerOptions() + .WithPluginAssemblies(assemblies) + .WithManualRegistrations(fixture.DelegateRegistration(container => + { + container.Register, TestDataRowExcelDataExtractor>(EnLifestyle.Transient); + container.Register, TestDataRowDataTableReader>(EnLifestyle.Transient); + })); + + DependencyContainer = fixture.DependencyContainer(options); + } + + private IDependencyContainer DependencyContainer { get; } + + [Fact] + internal void ExcelRoundRobinTest() + { + var exporter = DependencyContainer.Resolve(); + var importer = DependencyContainer.Resolve>(); + + var elements = new[] + { + new TestDataRow + { + BooleanField = true, + StringField = "SomeString", + NullableStringField = "SomeNullableString", + IntField = 42 + } + }; + + var sheetName = nameof(ExcelRoundRobinTest); + + var infos = new ISheetInfo[] + { + new FlatTableSheetInfo(elements) { SheetName = sheetName } + }; + + using (var stream = exporter.ExportXlsx(infos)) + { + var specification = new ExcelDataExtractorSpecification( + stream, + sheetName, + new Range(0, 1)); + + var importedElements = importer + .ExtractData(specification) + .ToList(); + + Assert.NotEmpty(elements); + Assert.NotEmpty(importedElements); + Assert.Equal(elements, importedElements); + } + } + + [ManuallyRegisteredComponent(nameof(DataExportImportTest))] + private class TestDataRowExcelDataExtractor : ExcelDataExtractor + { + public TestDataRowExcelDataExtractor( + IExcelCellValueExtractor cellValueExtractor, + IExcelColumnsSelectionBehavior columnsSelectionBehavior, + IDataTableReader dataTableReader) + : base(cellValueExtractor, columnsSelectionBehavior, dataTableReader) + { + } + } + + [ManuallyRegisteredComponent(nameof(DataExportImportTest))] + private class TestDataRowDataTableReader : DataTableReaderBase + { + public override IReadOnlyDictionary PropertyToColumnCaption { get; } = + new Dictionary(StringComparer.OrdinalIgnoreCase) + { + [nameof(TestDataRow.BooleanField)] = nameof(TestDataRow.BooleanField), + [nameof(TestDataRow.StringField)] = nameof(TestDataRow.StringField), + [nameof(TestDataRow.NullableStringField)] = nameof(TestDataRow.NullableStringField), + [nameof(TestDataRow.IntField)] = nameof(TestDataRow.IntField) + }; + + public override TestDataRow? ReadRow( + DataRow row, + int rowIndex, + IReadOnlyDictionary propertyToColumn, + ExcelTableMetadata tableMetadata) + { + if (RowIsEmpty(row, propertyToColumn)) + { + return default; + } + + var testDataRow = new TestDataRow + { + BooleanField = ReadRequiredBool(row, nameof(TestDataRow.BooleanField), propertyToColumn), + StringField = ReadRequiredString(row, nameof(TestDataRow.StringField), propertyToColumn), + NullableStringField = ReadString(row, nameof(TestDataRow.NullableStringField), propertyToColumn), + IntField = ReadRequiredInt(row, nameof(TestDataRow.IntField), propertyToColumn, new IFormatProvider[] { CultureInfo.InvariantCulture }) + }; + + return testDataRow; + } + } + + private class TestDataRow : IEquatable, + ISafelyEquatable + { + public bool BooleanField { get; set; } + + public string StringField { get; set; } = default!; + + public string? NullableStringField { get; set; } + + public int IntField { get; set; } + + #region IEquatable + + public static bool operator ==(TestDataRow? left, TestDataRow? right) + { + return Equatable.Equals(left, right); + } + + public static bool operator !=(TestDataRow? left, TestDataRow? right) + { + return !Equatable.Equals(left, right); + } + + public override int GetHashCode() + { + return HashCode.Combine( + BooleanField, + StringField.GetHashCode(StringComparison.Ordinal), + NullableStringField.GetHashCode(StringComparison.Ordinal), + IntField); + } + + public override bool Equals(object? obj) + { + return Equatable.Equals(this, obj); + } + + public bool Equals(TestDataRow? other) + { + return Equatable.Equals(this, other); + } + + public bool SafeEquals(TestDataRow other) + { + return BooleanField.Equals(other.BooleanField) + && string.Equals(StringField, other.StringField, StringComparison.Ordinal) + && string.Equals(NullableStringField, other.NullableStringField, StringComparison.Ordinal) + && IntField.Equals(other.IntField); + } + + #endregion + } + } +} \ No newline at end of file diff --git a/tests/Tests/Modules.Test/DynamicClassProviderTest.cs b/tests/Tests/Modules.Test/DynamicClassProviderTest.cs new file mode 100644 index 00000000..d71151c0 --- /dev/null +++ b/tests/Tests/Modules.Test/DynamicClassProviderTest.cs @@ -0,0 +1,220 @@ +namespace SpaceEngineers.Core.Modules.Test +{ + using System; + using System.Collections.Generic; + using System.Diagnostics.CodeAnalysis; + using System.Linq; + using System.Reflection; + using Basics; + using CompositionRoot; + using Core.Test.Api; + using Core.Test.Api.ClassFixtures; + using Dynamic; + using Dynamic.Abstractions; + using Xunit; + using Xunit.Abstractions; + + /// + /// Test for IDynamicClassProvider + /// + public class DynamicClassProviderTest : TestBase + { + /// .cctor + /// ITestOutputHelper + /// TestFixture + public DynamicClassProviderTest(ITestOutputHelper output, TestFixture fixture) + : base(output, fixture) + { + var assemblies = new[] + { + AssembliesExtensions.FindRequiredAssembly(AssembliesExtensions.BuildName(nameof(SpaceEngineers), nameof(Core), nameof(Dynamic))) + }; + + var options = new DependencyContainerOptions() + .WithPluginAssemblies(assemblies); + + DependencyContainer = fixture.DependencyContainer(options); + } + + private IDependencyContainer DependencyContainer { get; } + + /// DynamicClass test data member + /// Test data + public static IEnumerable DynamicClassTestData() + { + var emptyPropertyValues = new Dictionary(); + + var propertyValues = new Dictionary + { + [new DynamicProperty(typeof(bool), nameof(Boolean))] = true, + [new DynamicProperty(typeof(int), nameof(Int32))] = 42, + [new DynamicProperty(typeof(string), nameof(String))] = "qwerty" + }; + + var expectedFields = new[] + { + ("_boolean", typeof(bool)), + ("_int32", typeof(int)), + ("_string", typeof(string)), + }; + + var expectedProperties = new[] + { + (nameof(Boolean), typeof(bool)), + (nameof(Int32), typeof(int)), + (nameof(String), typeof(string)), + }; + + yield return new object[] + { + new Func(() => new DynamicClass($"{nameof(DynamicClassTestData)}_1")), + emptyPropertyValues, + new Action(type => + { + Assert.Equal(typeof(object), type.BaseType); + Assert.Empty(type.GetInterfaces()); + Assert.Empty(type.GetFields(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance)); + Assert.Empty(type.GetProperties(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.GetProperty | BindingFlags.SetProperty)); + }), + new Action(Assert.NotNull) + }; + yield return new object[] + { + new Func(() => new DynamicClass($"{nameof(DynamicClassTestData)}_2").InheritsFrom(typeof(TestBaseClass))), + emptyPropertyValues, + new Action(type => + { + Assert.Equal(typeof(TestBaseClass), type.BaseType); + Assert.Empty(type.GetInterfaces()); + Assert.Empty(type.GetFields(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance)); + Assert.Empty(type.GetProperties(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.GetProperty | BindingFlags.SetProperty)); + }), + new Action(Assert.NotNull) + }; + yield return new object[] + { + new Func(() => new DynamicClass($"{nameof(DynamicClassTestData)}_3").Implements(typeof(ITestInterface))), + emptyPropertyValues, + new Action(type => + { + Assert.Equal(typeof(object), type.BaseType); + Assert.Contains(typeof(ITestInterface), type.GetInterfaces()); + Assert.Empty(type.GetFields(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance)); + Assert.Empty(type.GetProperties(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.GetProperty | BindingFlags.SetProperty)); + }), + new Action(Assert.NotNull) + }; + yield return new object[] + { + new Func(() => new DynamicClass($"{nameof(DynamicClassTestData)}_4").HasProperties(propertyValues.Keys.ToArray())), + propertyValues, + new Action(type => + { + Assert.Equal(typeof(object), type.BaseType); + Assert.Empty(type.GetInterfaces()); + + var actualFields = type + .GetFields(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance) + .Select(field => (field.Name, field.FieldType)) + .OrderBy(field => field.Name); + + Assert.True(expectedFields.SequenceEqual(actualFields)); + + var actualProperties = type + .GetProperties(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.GetProperty | BindingFlags.SetProperty) + .Select(property => (property.Name, property.PropertyType)) + .OrderBy(property => property.Name); + + Assert.True(expectedProperties.SequenceEqual(actualProperties)); + }), + new Action(instance => + { + Assert.NotNull(instance); + + foreach (var (property, value) in propertyValues) + { + Assert.Equal(value, instance.GetPropertyValue(property.Name)); + } + }) + }; + yield return new object[] + { + new Func(() => new DynamicClass($"{nameof(DynamicClassTestData)}_5").InheritsFrom(typeof(TestBaseClass)).Implements(typeof(ITestInterface)).HasProperties(propertyValues.Keys.ToArray())), + propertyValues, + new Action(type => + { + Assert.Equal(typeof(TestBaseClass), type.BaseType); + Assert.Contains(typeof(ITestInterface), type.GetInterfaces()); + Assert.Single(type.GetInterfaces()); + + var actualFields = type + .GetFields(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance) + .Select(field => (field.Name, field.FieldType)) + .OrderBy(field => field.Name); + + Assert.True(expectedFields.SequenceEqual(actualFields)); + + var actualProperties = type + .GetProperties(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.GetProperty | BindingFlags.SetProperty) + .Select(property => (property.Name, property.PropertyType)) + .OrderBy(property => property.Name); + + Assert.True(expectedProperties.SequenceEqual(actualProperties)); + }), + new Action(instance => + { + Assert.NotNull(instance); + + foreach (var (property, value) in propertyValues) + { + Assert.Equal(value, instance.GetPropertyValue(property.Name)); + } + }) + }; + } + + [Theory] + [MemberData(nameof(DynamicClassTestData))] + internal void CacheTest( + Func factory, + IReadOnlyDictionary values, + Action assertType, + Action assertInstance) + { + var provider = DependencyContainer.Resolve(); + + var type = provider.CreateType(factory()); + var instance = provider.CreateInstance(factory(), values); + + type.GetProperties(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.GetProperty | BindingFlags.SetProperty) + .Each(property => Output.WriteLine($"{property.Name} ({property.PropertyType.Name}) - {property.GetValue(instance)?.ToString() ?? "null"}")); + + type.GetFields(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance) + .Each(field => Output.WriteLine($"{field.Name} ({field.FieldType.Name}) - {field.GetValue(instance)?.ToString() ?? "null"}")); + + assertType(type); + assertInstance(instance); + + Assert.Equal(type, provider.CreateType(factory())); + Assert.Equal(type, Activator.CreateInstance(type).GetType()); + Assert.Equal(type, provider.CreateInstance(factory(), values).GetType()); + } + + /// + /// ITestInterface + /// + [SuppressMessage("Analysis", "CA1034", Justification = "for test reasons")] + [SuppressMessage("Analysis", "SA1201", Justification = "for test reasons")] + public interface ITestInterface + { + } + + /// + /// TestBaseClass + /// + [SuppressMessage("Analysis", "CA1034", Justification = "for test reasons")] + public class TestBaseClass + { + } + } +} \ No newline at end of file diff --git a/tests/Tests/Modules.Test/Modules.Test.csproj b/tests/Tests/Modules.Test/Modules.Test.csproj new file mode 100644 index 00000000..37be94ab --- /dev/null +++ b/tests/Tests/Modules.Test/Modules.Test.csproj @@ -0,0 +1,57 @@ + + + + net7.0 + SpaceEngineers.Core.Modules.Test + SpaceEngineers.Core.Modules.Test + false + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + + ICollectionResolvableConditionDecorableServiceDecorator.cs + + + ICollectionResolvableConditionDecorableServiceDecorator.cs + + + ICollectionResolvableConditionDecorableServiceDecorator.cs + + + IConditionalDecorableServiceDecorator.cs + + + IConditionalDecorableServiceDecorator.cs + + + IConditionalDecorableServiceDecorator.cs + + + BaseEventEmptyMessageHandler.cs + + + \ No newline at end of file diff --git a/tests/Tests/Modules.Test/Properties/AssemblyInfo.cs b/tests/Tests/Modules.Test/Properties/AssemblyInfo.cs new file mode 100644 index 00000000..31adb06d --- /dev/null +++ b/tests/Tests/Modules.Test/Properties/AssemblyInfo.cs @@ -0,0 +1,5 @@ +using System.Reflection; + +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] +[assembly: AssemblyInformationalVersion("1.0.0.0")] \ No newline at end of file diff --git a/tests/Tests/Modules.Test/SerializationTest.cs b/tests/Tests/Modules.Test/SerializationTest.cs new file mode 100644 index 00000000..4ee240d0 --- /dev/null +++ b/tests/Tests/Modules.Test/SerializationTest.cs @@ -0,0 +1,190 @@ +namespace SpaceEngineers.Core.Modules.Test +{ + using System; + using System.Globalization; + using System.Linq; + using System.Text.Json.Nodes; + using System.Text.Json.Serialization; + using Basics; + using CompositionRoot; + using Core.Test.Api; + using Core.Test.Api.ClassFixtures; + using CrossCuttingConcerns.Json; + using CrossCuttingConcerns.Settings; + using Xunit; + using Xunit.Abstractions; + + /// + /// SerializationTest + /// Json module + /// + public class SerializationTest : TestBase + { + /// .ctor + /// ITestOutputHelper + /// TestFixture + public SerializationTest(ITestOutputHelper output, TestFixture fixture) + : base(output, fixture) + { + var projectFileDirectory = SolutionExtensions.ProjectFile().Directory + ?? throw new InvalidOperationException("Project directory wasn't found"); + + var settingsDirectory = projectFileDirectory.StepInto("Settings"); + + var assemblies = new[] + { + AssembliesExtensions.FindRequiredAssembly(AssembliesExtensions.BuildName(nameof(SpaceEngineers), nameof(Core), nameof(CrossCuttingConcerns))) + }; + + var options = new DependencyContainerOptions() + .WithPluginAssemblies(assemblies) + .WithManualRegistrations(new SettingsDirectoryProviderManualRegistration(new SettingsDirectoryProvider(settingsDirectory))); + + DependencyContainer = fixture.DependencyContainer(options); + } + + private IDependencyContainer DependencyContainer { get; } + + [Fact] + internal void TypeSerializationTest() + { + var serializer = DependencyContainer.Resolve(); + + var serialized = serializer.SerializeObject(typeof(object), typeof(Type)); + Output.WriteLine(serialized); + Assert.Equal(typeof(object), serializer.DeserializeObject(serialized)); + + serialized = serializer.SerializeObject(TypeNode.FromType(typeof(object)), typeof(TypeNode)); + Output.WriteLine(serialized); + Assert.Equal(typeof(object), TypeNode.ToType(serializer.DeserializeObject(serialized))); + } + + [Fact] + internal void PolymorphicSerializationTest() + { + var str = "qwerty"; + object obj = str; + + var payload = new + { + Str = str, + Obj = obj + }; + + var serializer = DependencyContainer.Resolve(); + + var serialized = serializer.SerializeObject(payload, payload.GetType()); + Output.WriteLine(serialized); + var deserialized = serializer.DeserializeObject(serialized, payload.GetType()); + + Assert.NotNull(deserialized); + Assert.NotNull(deserialized.GetPropertyValue("Str")); + Assert.NotNull(deserialized.GetPropertyValue("Obj")); + } + + [Theory] + [InlineData($@"{{""Id"": 42}}")] + [InlineData($@"{{""Id"": 42, ""$id"": ""1"", ""$type"": ""SpaceEngineers.Core.Modules.Test SpaceEngineers.Core.Modules.Test.SerializationTest+TestMetadata""}}")] + [InlineData($@"{{""$id"": ""1"", ""Id"": 42, ""$type"": ""SpaceEngineers.Core.Modules.Test SpaceEngineers.Core.Modules.Test.SerializationTest+TestMetadata""}}")] + [InlineData($@"{{""$id"": ""1"", ""$type"": ""SpaceEngineers.Core.Modules.Test SpaceEngineers.Core.Modules.Test.SerializationTest+TestMetadata"", ""Id"": 42}}")] + [InlineData($@"{{""Id"": 42, ""Inner"": {{""Id"": 43 }}}}")] + [InlineData($@"{{""Id"": 42, ""Inner"": {{""Id"": 43 }}, ""$id"": ""1"", ""$type"": ""SpaceEngineers.Core.Modules.Test SpaceEngineers.Core.Modules.Test.SerializationTest+TestMetadata""}}")] + [InlineData($@"{{""$id"": ""1"", ""Id"": 42, ""Inner"": {{""Id"": 43 }}, ""$type"": ""SpaceEngineers.Core.Modules.Test SpaceEngineers.Core.Modules.Test.SerializationTest+TestMetadata""}}")] + [InlineData($@"{{""$id"": ""1"", ""$type"": ""SpaceEngineers.Core.Modules.Test SpaceEngineers.Core.Modules.Test.SerializationTest+TestMetadata"", ""Id"": 42, ""Inner"": {{""Id"": 43 }}}}")] + [InlineData($@"{{""Id"": 42, ""Inner"": {{""Id"": 43, ""$id"": ""2"", ""$type"": ""SpaceEngineers.Core.Modules.Test SpaceEngineers.Core.Modules.Test.SerializationTest+TestMetadata"" }}}}")] + [InlineData($@"{{""Id"": 42, ""Inner"": {{""Id"": 43, ""$id"": ""2"", ""$type"": ""SpaceEngineers.Core.Modules.Test SpaceEngineers.Core.Modules.Test.SerializationTest+TestMetadata"" }}, ""$id"": ""1"", ""$type"": ""SpaceEngineers.Core.Modules.Test SpaceEngineers.Core.Modules.Test.SerializationTest+TestMetadata""}}")] + [InlineData($@"{{""$id"": ""1"", ""Id"": 42, ""Inner"": {{""Id"": 43, ""$id"": ""2"", ""$type"": ""SpaceEngineers.Core.Modules.Test SpaceEngineers.Core.Modules.Test.SerializationTest+TestMetadata"" }}, ""$type"": ""SpaceEngineers.Core.Modules.Test SpaceEngineers.Core.Modules.Test.SerializationTest+TestMetadata""}}")] + [InlineData($@"{{""$id"": ""1"", ""$type"": ""SpaceEngineers.Core.Modules.Test SpaceEngineers.Core.Modules.Test.SerializationTest+TestMetadata"", ""Id"": 42, ""Inner"": {{""Id"": 43, ""$id"": ""2"", ""$type"": ""SpaceEngineers.Core.Modules.Test SpaceEngineers.Core.Modules.Test.SerializationTest+TestMetadata"" }}}}")] + internal void JsonMetadataDeserializationTest(string json) + { + Output.WriteLine(json); + + var obj = DependencyContainer + .Resolve() + .DeserializeObject(json); + + Assert.Equal(42, obj.Id); + } + + [Fact] + internal void ObjectTreeDeserializationTest() + { + var serialized = @"{ +""candles"": { + ""metadata"": { + ""open"": {""type"": ""double""}, + ""close"": {""type"": ""double""}, + ""high"": {""type"": ""double""}, + ""low"": {""type"": ""double""}, + ""value"": {""type"": ""double""}, + ""volume"": {""type"": ""double""}, + ""begin"": {""type"": ""datetime"", ""bytes"": 19, ""max_size"": 0}, + ""end"": {""type"": ""datetime"", ""bytes"": 19, ""max_size"": 0} + }, + ""columns"": [""open"", ""close"", ""high"", ""low"", ""value"", ""volume"", ""begin"", ""end""], + ""data"": [ + [82.14, 82.14, 82.14, 82.14, 4930864.2, 60030, ""2020-09-16 09:00:00"", ""2020-09-16 09:59:59""], + [82.38, 83.14, 83.76, 82.24, 204154615.8, 2456970, ""2020-09-16 10:00:00"", ""2020-09-16 10:59:59""], + [83.14, 83.08, 83.36, 82.92, 52378085.99999999, 630140, ""2020-09-16 11:00:00"", ""2020-09-16 11:59:59""], + [83.06, 82.96, 83.14, 82.9, 33313677.200000003, 401210, ""2020-09-16 12:00:00"", ""2020-09-16 12:59:59""], + [82.96, 82.92, 83.1, 82.86, 38844514.79999998, 468140, ""2020-09-16 13:00:00"", ""2020-09-16 13:59:59""], + [82.94, 83.04, 83.04, 82.84, 20225836.19999999, 243760, ""2020-09-16 14:00:00"", ""2020-09-16 14:59:59""], + [83.06, 82.86, 83.12, 82.7, 25191517.6, 303770, ""2020-09-16 15:00:00"", ""2020-09-16 15:59:59""], + [82.94, 82.62, 83, 82.54, 46140851.6, 557740, ""2020-09-16 16:00:00"", ""2020-09-16 16:59:59""], + [82.64, 82.78, 82.96, 82.52, 42417728.39999998, 512440, ""2020-09-16 17:00:00"", ""2020-09-16 17:59:59""], + [82.84, 82.9, 82.9, 82.62, 18994727.8, 229340, ""2020-09-16 18:00:00"", ""2020-09-16 18:48:11""], + [82.58, 82.86, 82.94, 82.58, 5006094.6, 60490, ""2020-09-16 19:00:00"", ""2020-09-16 19:59:59""], + [82.86, 82.78, 83, 82.74, 14463095.8, 174470, ""2020-09-16 20:00:00"", ""2020-09-16 20:59:59""], + [82.74, 82.68, 82.84, 82.64, 6486566.600000001, 78350, ""2020-09-16 21:00:00"", ""2020-09-16 21:59:59""] + ] +}}"; + + var culture = CultureInfo.GetCultureInfo("en-US"); + var serializer = DependencyContainer.Resolve(); + + var node = serializer.DeserializeObject(serialized) as JsonObject; + Assert.NotNull(node); + + Assert.True(node.ContainsKey("candles")); + var candles = node["candles"] as JsonObject; + Assert.NotNull(candles); + + Assert.True(candles.ContainsKey("data")); + var rows = candles["data"] as JsonArray; + Assert.NotNull(rows); + Assert.Equal(13, rows.Count); + + var row = rows.Last() as JsonArray; + Assert.NotNull(row); + Assert.Equal(8, row.Count); + + var number = (row.First() as JsonValue)?.ToString(); + Assert.NotNull(number); + Assert.Equal(82.74, double.Parse(number, culture)); + + var strDate = (row.Last() as JsonValue)?.ToString(); + Assert.NotNull(strDate); + Assert.Equal(new DateTime(2020, 09, 16, 21, 59, 59), Convert.ToDateTime(strDate, culture)); + } + + internal class TestMetadata + { + [JsonConstructor] + public TestMetadata() + { + } + + public TestMetadata( + int id, + TestMetadata? inner) + { + Id = id; + Inner = inner; + } + + public int Id { get; init; } + + public TestMetadata? Inner { get; init; } + } + } +} \ No newline at end of file diff --git a/tests/Tests/Modules.Test/Settings/ReadSettingsTest/appsettings.json b/tests/Tests/Modules.Test/Settings/ReadSettingsTest/appsettings.json new file mode 100644 index 00000000..6fd363be --- /dev/null +++ b/tests/Tests/Modules.Test/Settings/ReadSettingsTest/appsettings.json @@ -0,0 +1,39 @@ +{ + "TestConfigurationSettings": { + "Int": 42, + "Decimal": 42.42, + "String": "Hello world!", + "Date": "2000-01-01T00:00:00", + "Reference": { + "Int": 42, + "Decimal": 42.42, + "String": "Hello world!", + "Date": "2000-01-01T00:00:00", + "Reference": null, + "Collection": null, + "Dictionary": null + }, + "Collection": [ + { + "Int": 42, + "Decimal": 42.42, + "String": "Hello world!", + "Date": "2000-01-01T00:00:00", + "Reference": null, + "Collection": null, + "Dictionary": null + } + ], + "Dictionary": { + "First": { + "Int": 42, + "Decimal": 42.42, + "String": "Hello world!", + "Date": "2000-01-01T00:00:00", + "Reference": null, + "Collection": null, + "Dictionary": null + } + } + } +} \ No newline at end of file diff --git a/tests/Tests/Modules.Test/Settings/TestConfigurationSettings.cs b/tests/Tests/Modules.Test/Settings/TestConfigurationSettings.cs new file mode 100644 index 00000000..b99cf9df --- /dev/null +++ b/tests/Tests/Modules.Test/Settings/TestConfigurationSettings.cs @@ -0,0 +1,23 @@ +namespace SpaceEngineers.Core.Modules.Test.Settings +{ + using System; + using System.Collections.Generic; + using CrossCuttingConcerns.Settings; + + internal class TestConfigurationSettings : ISettings + { + public int Int { get; init; } + + public decimal Decimal { get; init; } + + public string? String { get; init; } + + public DateTime Date { get; init; } + + public TestConfigurationSettings? Reference { get; init; } + + public ICollection? Collection { get; init; } + + public IDictionary? Dictionary { get; init; } + } +} \ No newline at end of file diff --git a/tests/Tests/Modules.Test/Settings/appsettings.json b/tests/Tests/Modules.Test/Settings/appsettings.json new file mode 100644 index 00000000..7bfabc43 --- /dev/null +++ b/tests/Tests/Modules.Test/Settings/appsettings.json @@ -0,0 +1,10 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Debug", + "System": "Information", + "Microsoft": "Information", + "Npgsql": "Information" + } + } +} \ No newline at end of file diff --git a/tests/Tests/Modules.Test/SettingsProviderTest.cs b/tests/Tests/Modules.Test/SettingsProviderTest.cs new file mode 100644 index 00000000..302052fb --- /dev/null +++ b/tests/Tests/Modules.Test/SettingsProviderTest.cs @@ -0,0 +1,101 @@ +namespace SpaceEngineers.Core.Modules.Test +{ + using System; + using System.Linq; + using System.Reflection; + using AutoRegistration.Api.Enumerations; + using Basics; + using CompositionRoot; + using Core.Test.Api; + using Core.Test.Api.ClassFixtures; + using CrossCuttingConcerns.Settings; + using Settings; + using Xunit; + using Xunit.Abstractions; + + /// + /// ISettingsProvider class tests + /// + public class SettingsProviderTest : TestBase + { + /// .ctor + /// ITestOutputHelper + /// TestFixture + public SettingsProviderTest(ITestOutputHelper output, TestFixture fixture) + : base(output, fixture) + { + var projectFileDirectory = SolutionExtensions.ProjectFile().Directory + ?? throw new InvalidOperationException("Project directory wasn't found"); + + var settingsDirectory = projectFileDirectory + .StepInto("Settings") + .StepInto(nameof(ReadSettingsTest)); + + var assemblies = new[] + { + AssembliesExtensions.FindRequiredAssembly(AssembliesExtensions.BuildName(nameof(SpaceEngineers), nameof(Core), nameof(CrossCuttingConcerns))) + }; + + var additionalOurTypes = new[] + { + typeof(TestConfigurationSettings) + }; + + var options = new DependencyContainerOptions() + .WithPluginAssemblies(assemblies) + .WithAdditionalOurTypes(additionalOurTypes) + .WithManualRegistrations(new SettingsDirectoryProviderManualRegistration(new SettingsDirectoryProvider(settingsDirectory))) + .WithManualRegistrations(Fixture.DelegateRegistration(container => + { + container.Register, AppSettingsJsonSettingsProvider>(EnLifestyle.Singleton); + })); + + DependencyContainer = Fixture.DependencyContainer(options); + } + + private IDependencyContainer DependencyContainer { get; } + + [Fact] + internal void ReadSettingsTest() + { + var settings = DependencyContainer + .Resolve>() + .Get(); + + Assert.NotNull(settings); + + Output.WriteLine(settings.Dump(BindingFlags.Instance | BindingFlags.Public)); + Output.WriteLine(string.Empty); + + AssertPrimitives(settings); + + Assert.NotNull(settings.Reference); + AssertCircularReference(settings.Reference!); + + Assert.NotNull(settings.Collection); + Assert.Single(settings.Collection!); + AssertCircularReference(settings.Collection!.Single()); + + Assert.NotNull(settings.Dictionary); + Assert.Single(settings.Dictionary!); + Assert.Equal("First", settings.Dictionary!.Single().Key); + AssertCircularReference(settings.Dictionary!.Single().Value); + + static void AssertPrimitives(TestConfigurationSettings settings) + { + Assert.Equal(42, settings.Int); + Assert.Equal(42.42m, settings.Decimal); + Assert.Equal("Hello world!", settings.String); + Assert.Equal(new DateTime(2000, 1, 1), settings.Date); + } + + static void AssertCircularReference(TestConfigurationSettings settings) + { + AssertPrimitives(settings); + Assert.Null(settings.Reference); + Assert.Null(settings.Collection); + Assert.Null(settings.Dictionary); + } + } + } +} \ No newline at end of file diff --git a/tests/Tests/Roslyn.Test/Abstractions/ICodeFixVerifier.cs b/tests/Tests/Roslyn.Test/Abstractions/ICodeFixVerifier.cs new file mode 100644 index 00000000..16cf9962 --- /dev/null +++ b/tests/Tests/Roslyn.Test/Abstractions/ICodeFixVerifier.cs @@ -0,0 +1,29 @@ +namespace SpaceEngineers.Core.Roslyn.Test.Abstractions +{ + using System; + using System.Threading.Tasks; + using Microsoft.CodeAnalysis.CodeFixes; + using Microsoft.CodeAnalysis.Diagnostics; + using ValueObjects; + + /// + /// Verifies code fix results + /// + public interface ICodeFixVerifier + { + /// + /// Verify code fix results + /// + /// DiagnosticAnalyzer + /// CodeFixProvider + /// AnalyzedDocument + /// CodeFix expected source + /// Show result + /// Ongoing operation + Task VerifyCodeFix(DiagnosticAnalyzer analyzer, + CodeFixProvider codeFix, + AnalyzedDocument analyzedDocument, + SourceFile expectedSource, + Action show); + } +} \ No newline at end of file diff --git a/tests/Tests/Roslyn.Test/Abstractions/IConventionalProvider.cs b/tests/Tests/Roslyn.Test/Abstractions/IConventionalProvider.cs new file mode 100644 index 00000000..da611505 --- /dev/null +++ b/tests/Tests/Roslyn.Test/Abstractions/IConventionalProvider.cs @@ -0,0 +1,35 @@ +namespace SpaceEngineers.Core.Roslyn.Test.Abstractions +{ + using System.Collections.Generic; + using Microsoft.CodeAnalysis.CodeFixes; + using Microsoft.CodeAnalysis.Diagnostics; + using ValueObjects; + + /// + /// Analysis objects provider base on conventions + /// + public interface IConventionalProvider + { + /// + /// GetCodeFixProvider + /// + /// DiagnosticAnalyzer + /// CodeFixProvider + CodeFixProvider? CodeFixProvider(DiagnosticAnalyzer analyzer); + + /// + /// GetExpectedDiagnosticsProvider + /// + /// DiagnosticAnalyzer + /// IExpectedDiagnosticsProvider + IExpectedDiagnosticsProvider ExpectedDiagnosticsProvider(DiagnosticAnalyzer analyzer); + + /// + /// GetSourceFile + /// + /// DiagnosticAnalyzer + /// DirectorySuffix + /// SourceFiles + public IEnumerable SourceFiles(DiagnosticAnalyzer analyzer, string? directorySuffix = null); + } +} \ No newline at end of file diff --git a/tests/Tests/Roslyn.Test/Abstractions/IDiagnosticAnalyzerVerifier.cs b/tests/Tests/Roslyn.Test/Abstractions/IDiagnosticAnalyzerVerifier.cs new file mode 100644 index 00000000..1061d92b --- /dev/null +++ b/tests/Tests/Roslyn.Test/Abstractions/IDiagnosticAnalyzerVerifier.cs @@ -0,0 +1,23 @@ +namespace SpaceEngineers.Core.Roslyn.Test.Abstractions +{ + using System.Collections.Immutable; + using Analyzers.Api; + using ValueObjects; + + /// + /// Verify correctness of DiagnosticAnalyzer + /// + public interface IDiagnosticAnalyzerVerifier + { + /// + /// VerifyDiagnostics by IExpectedResultsProvider + /// + /// The analyzer that was being run on the sources + /// AnalyzedDocument + /// Expected diagnostics + public void VerifyAnalyzedDocument( + SyntaxAnalyzerBase analyzer, + AnalyzedDocument analyzedDocument, + ImmutableArray expectedDiagnostics); + } +} \ No newline at end of file diff --git a/tests/Tests/Roslyn.Test/Abstractions/IExpectedDiagnosticsProvider.cs b/tests/Tests/Roslyn.Test/Abstractions/IExpectedDiagnosticsProvider.cs new file mode 100644 index 00000000..08611e24 --- /dev/null +++ b/tests/Tests/Roslyn.Test/Abstractions/IExpectedDiagnosticsProvider.cs @@ -0,0 +1,20 @@ +namespace SpaceEngineers.Core.Roslyn.Test.Abstractions +{ + using System.Collections.Generic; + using System.Collections.Immutable; + using Analyzers.Api; + using ValueObjects; + + /// + /// IExpectedResultsProvider + /// + public interface IExpectedDiagnosticsProvider + { + /// + /// Gets expected diagnostics + /// + /// Identified diagnostic analyzer + /// Expected diagnostics + IDictionary> ByFileName(SyntaxAnalyzerBase analyzer); + } +} \ No newline at end of file diff --git a/tests/Tests/Roslyn.Test/Abstractions/IMetadataReferenceProvider.cs b/tests/Tests/Roslyn.Test/Abstractions/IMetadataReferenceProvider.cs new file mode 100644 index 00000000..834386c7 --- /dev/null +++ b/tests/Tests/Roslyn.Test/Abstractions/IMetadataReferenceProvider.cs @@ -0,0 +1,16 @@ +namespace SpaceEngineers.Core.Roslyn.Test.Abstractions +{ + using System.Collections.Generic; + using AutoRegistration.Api.Abstractions; + using Microsoft.CodeAnalysis; + + /// + /// MetadataReference provider for test sources + /// + public interface IMetadataReferenceProvider : ICollectionResolvable + { + /// Receive MetadataReferences + /// MetadataReference collection + IEnumerable ReceiveReferences(); + } +} \ No newline at end of file diff --git a/tests/Tests/Roslyn.Test/Abstractions/ISourceTransformer.cs b/tests/Tests/Roslyn.Test/Abstractions/ISourceTransformer.cs new file mode 100644 index 00000000..1883e7ab --- /dev/null +++ b/tests/Tests/Roslyn.Test/Abstractions/ISourceTransformer.cs @@ -0,0 +1,17 @@ +namespace SpaceEngineers.Core.Roslyn.Test.Abstractions +{ + using Microsoft.CodeAnalysis.Text; + + /// + /// IAliasHandler + /// + public interface ISourceTransformer + { + /// + /// Transform source + /// + /// Input source + /// Transformed source + SourceText Transform(SourceText source); + } +} \ No newline at end of file diff --git a/tests/Tests/Roslyn.Test/Extensions/AnalysisExtensions.cs b/tests/Tests/Roslyn.Test/Extensions/AnalysisExtensions.cs new file mode 100644 index 00000000..77c1fc1c --- /dev/null +++ b/tests/Tests/Roslyn.Test/Extensions/AnalysisExtensions.cs @@ -0,0 +1,165 @@ +namespace SpaceEngineers.Core.Roslyn.Test.Extensions +{ + using System; + using System.Collections.Generic; + using System.Collections.Immutable; + using System.Linq; + using System.Threading; + using System.Threading.Tasks; + using Basics.Exceptions; + using Microsoft.CodeAnalysis; + using Microsoft.CodeAnalysis.CSharp; + using Microsoft.CodeAnalysis.CSharp.Syntax; + using Microsoft.CodeAnalysis.Diagnostics; + using ValueObjects; + + internal static class AnalysisExtensions + { + internal const string CSharpDefaultFileExt = ".cs"; + + internal static Solution SetupSolution(this Solution solution, + string projectName, + IEnumerable sources, + IEnumerable metadataReferences) + { + var projectId = ProjectId.CreateNewId(projectName); + + var projectInfo = ProjectInfo.Create(projectId, + VersionStamp.Default, + projectName, + projectName, + LanguageNames.CSharp, + parseOptions: new CSharpParseOptions(LanguageVersion.Latest), + metadataReferences: metadataReferences); + + solution = solution.AddProject(projectInfo); + + foreach (var source in sources) + { + solution = solution.AddDocument(projectId, source); + } + + return solution; + } + + internal static async IAsyncEnumerable CompileSolution( + this Solution solution, + ImmutableArray analyzers, + ImmutableArray ignoredProjects, + ImmutableArray ignoredSources, + ImmutableArray ignoredNamespaces) + { + var projectTree = solution.GetProjectDependencyGraph(); + foreach (var projectId in projectTree.GetTopologicallySortedProjects()) + { + var project = solution.GetProject(projectId) + ?? throw new InvalidOperationException($"Project with {projectId} must exist in solution"); + + await foreach (var diagnostic in CompileProject(project, analyzers) + .WithCancellation(CancellationToken.None) + .ConfigureAwait(false)) + { + if (IsNotIgnoredProject(project) + && IsNotIgnoredSource(diagnostic) + && IsNotIgnoredNamespace(diagnostic)) + { + yield return diagnostic; + } + } + } + + bool IsNotIgnoredProject(Project project) + { + return !ignoredProjects.Contains(project.Name, StringComparer.OrdinalIgnoreCase); + } + + bool IsNotIgnoredSource(AnalyzedDocument analyzedDocument) + { + return ignoredSources.All(p => !analyzedDocument.Name.Contains(p, StringComparison.OrdinalIgnoreCase)); + } + + bool IsNotIgnoredNamespace(AnalyzedDocument analyzedDocument) + { + var syntaxTree = analyzedDocument.Document.GetSyntaxTreeAsync().Result; + + if (syntaxTree?.GetRoot() is not CompilationUnitSyntax cus) + { + return true; + } + + var namespaceSyntax = cus.Members.OfType().SingleOrDefault(); + + if (namespaceSyntax == null) + { + return true; + } + + var @namespace = namespaceSyntax.Name.ToString(); + + return !ignoredNamespaces.Any(n => n == @namespace); + } + } + + private static async IAsyncEnumerable CompileProject( + this Project project, + ImmutableArray analyzers) + { + var options = project.CompilationOptions + .WithPlatform(Platform.AnyCpu) + .WithConcurrentBuild(false) + .WithOptimizationLevel(OptimizationLevel.Debug) + .WithOutputKind(OutputKind.DynamicallyLinkedLibrary) + .WithAssemblyIdentityComparer(DesktopAssemblyIdentityComparer.Default) + .WithGeneralDiagnosticOption(ReportDiagnostic.Error); + + project = project.WithCompilationOptions(options); + + var compilation = await project.GetCompilationAsync().ConfigureAwait(false); + + foreach (var analyzedDocument in GroupByDocument(project, compilation.GetDiagnostics())) + { + yield return analyzedDocument; + } + + if (!analyzers.Any()) + { + yield break; + } + + var analyzerDiagnostics = await compilation.WithAnalyzers(analyzers) + .GetAnalyzerDiagnosticsAsync() + .ConfigureAwait(false); + + foreach (var analyzedDocument in GroupByDocument(project, analyzerDiagnostics)) + { + yield return analyzedDocument; + } + } + + private static Solution AddDocument(this Solution solution, ProjectId projectId, SourceFile sourceFile) + { + var sourceFileName = sourceFile.Name + CSharpDefaultFileExt; + var documentId = DocumentId.CreateNewId(projectId, sourceFileName); + return solution.AddDocument(documentId, sourceFileName, sourceFile.Text); + } + + private static IEnumerable GroupByDocument(Project project, IEnumerable source) + { + return source + .Select(diagnostic => + { + var document = project.GetDocument(diagnostic.Location.SourceTree); + + if (document == null) + { + throw new NotFoundException("Couldn't find document"); + } + + return (document, diagnostic); + }) + .GroupBy(pair => pair.document, + pair => pair.diagnostic) + .Select(grp => new AnalyzedDocument(grp.Key, ImmutableArray.Create(grp.ToArray()))); + } + } +} \ No newline at end of file diff --git a/tests/Tests/Roslyn.Test/Extensions/MetadataReferenceExtensions.cs b/tests/Tests/Roslyn.Test/Extensions/MetadataReferenceExtensions.cs new file mode 100644 index 00000000..15641e00 --- /dev/null +++ b/tests/Tests/Roslyn.Test/Extensions/MetadataReferenceExtensions.cs @@ -0,0 +1,28 @@ +namespace SpaceEngineers.Core.Roslyn.Test.Extensions +{ + using System.IO; + using System.Reflection; + using Microsoft.CodeAnalysis; + + /// + /// MetadataReferenceExtensions + /// + public static class MetadataReferenceExtensions + { + /// Creates MetadataReference from assembly + /// Assembly + /// MetadataReference + public static MetadataReference AsMetadataReference(this Assembly assembly) + { + return MetadataReference.CreateFromFile(assembly.Location); + } + + /// Creates MetadataReference from assembly file location + /// Assembly file location + /// MetadataReference + public static MetadataReference AsMetadataReference(this FileInfo assemblyFile) + { + return MetadataReference.CreateFromFile(assemblyFile.FullName); + } + } +} \ No newline at end of file diff --git a/tests/Tests/Roslyn.Test/Implementations/AliasSourceTransformer.cs b/tests/Tests/Roslyn.Test/Implementations/AliasSourceTransformer.cs new file mode 100644 index 00000000..bb4feafd --- /dev/null +++ b/tests/Tests/Roslyn.Test/Implementations/AliasSourceTransformer.cs @@ -0,0 +1,89 @@ +namespace SpaceEngineers.Core.Roslyn.Test.Implementations +{ + using System; + using System.Diagnostics.CodeAnalysis; + using System.Linq; + using System.Xml.Linq; + using Abstractions; + using AutoRegistration.Api.Abstractions; + using AutoRegistration.Api.Attributes; + using AutoRegistration.Api.Enumerations; + using Basics; + using Microsoft.CodeAnalysis; + using Microsoft.CodeAnalysis.CSharp; + using Microsoft.CodeAnalysis.Text; + + [Component(EnLifestyle.Singleton)] + internal class AliasSourceTransformer : ISourceTransformer, + ICollectionResolvable + { + public AliasSourceTransformer() + { + } + + public SourceText Transform(SourceText source) + { + while (TryGetReplacement(source, out var replacement)) + { + var (content, span) = replacement; + source = source.Replace(span, content); + } + + return ReplaceExpectedSuffix(source); + } + + private static SourceText ReplaceExpectedSuffix(SourceText source) + { + return SourceText.From(source.ToString().Replace(Conventions.Expected, string.Empty, StringComparison.Ordinal)); + } + + private static bool TryGetReplacement(SourceText source, out (string, TextSpan) replacement) + { + var syntaxTree = CSharpSyntaxTree.ParseText(source); + var replacements = ((SyntaxNodeOrToken)syntaxTree.GetRoot()) + .Flatten(node => node.ChildNodesAndTokens()) + .SelectMany(node => node.GetLeadingTrivia().Concat(node.GetTrailingTrivia())) + .Where(trivia => trivia.IsKind(SyntaxKind.MultiLineCommentTrivia)) + .Select(trivia => TryGetAlias(trivia.ToFullString(), out var content) + ? (content, trivia.Span) + : default) + .Where(pair => pair != default) + .ToList(); + + if (replacements.Any()) + { + replacement = replacements.First(); + return true; + } + + replacement = default; + return false; + } + + private static bool TryGetAlias(string comment, [NotNullWhen(true)] out string? content) + { + content = ExecutionExtensions + .Try(GetAlias, comment) + .Catch() + .Invoke(_ => default); + + return content != default; + } + + private static string? GetAlias(string comment) + { + var element = XElement.Parse(comment.Trim('/').Trim('*').Trim()); + + if (element.Name != Conventions.Analyzer) + { + return default; + } + + var analyzerName = element.Attribute(Conventions.NameAttribute)?.Value; + + return analyzerName.IsNullOrEmpty() + ? default + : element.Value; + } + } +} \ No newline at end of file diff --git a/tests/Tests/Roslyn.Test/Implementations/CodeFixVerifier.cs b/tests/Tests/Roslyn.Test/Implementations/CodeFixVerifier.cs new file mode 100644 index 00000000..bd7e9b0c --- /dev/null +++ b/tests/Tests/Roslyn.Test/Implementations/CodeFixVerifier.cs @@ -0,0 +1,77 @@ +namespace SpaceEngineers.Core.Roslyn.Test.Implementations +{ + using System; + using System.Collections.Generic; + using System.Collections.Immutable; + using System.Linq; + using System.Threading; + using System.Threading.Tasks; + using Abstractions; + using AutoRegistration.Api.Abstractions; + using AutoRegistration.Api.Attributes; + using AutoRegistration.Api.Enumerations; + using Microsoft.CodeAnalysis; + using Microsoft.CodeAnalysis.CodeActions; + using Microsoft.CodeAnalysis.CodeFixes; + using Microsoft.CodeAnalysis.Diagnostics; + using ValueObjects; + using Xunit; + + [Component(EnLifestyle.Singleton)] + internal class CodeFixVerifier : ICodeFixVerifier, + IResolvable + { + public async Task VerifyCodeFix(DiagnosticAnalyzer analyzer, + CodeFixProvider codeFix, + AnalyzedDocument analyzedDocument, + SourceFile expectedSource, + Action show) + { + var fixedDocument = await ApplyFix(codeFix, analyzedDocument.Document, analyzedDocument.ActualDiagnostics).ConfigureAwait(false); + var actualSource = await fixedDocument.GetTextAsync().ConfigureAwait(false); + + if (expectedSource.Text.ToString() != actualSource.ToString()) + { + show("Expected: " + expectedSource.Text); + show("Actual: " + actualSource); + } + + Assert.Equal(expectedSource.Text.ToString(), actualSource.ToString()); + } + + private static async Task ApplyFix( + CodeFixProvider codeFix, + Document document, + ImmutableArray actualDiagnostics) + { + var actions = new List(); + + foreach (var diagnostic in actualDiagnostics) + { + var context = new CodeFixContext( + document, + diagnostic, + (a, d) => actions.Add(a), + CancellationToken.None); + + await codeFix + .RegisterCodeFixesAsync(context) + .ConfigureAwait(false); + } + + foreach (var codeAction in actions) + { + document = (await codeAction + .GetOperationsAsync(CancellationToken.None) + .ConfigureAwait(false)) + .OfType() + .Single() + .ChangedSolution + .GetDocument(document.Id) + ?? throw new InvalidOperationException($"{nameof(ApplyChangesOperation.ChangedSolution)} must contains document {document.Id}"); + } + + return document; + } + } +} diff --git a/tests/Tests/Roslyn.Test/Implementations/ConventionalProvider.cs b/tests/Tests/Roslyn.Test/Implementations/ConventionalProvider.cs new file mode 100644 index 00000000..d56ba218 --- /dev/null +++ b/tests/Tests/Roslyn.Test/Implementations/ConventionalProvider.cs @@ -0,0 +1,91 @@ +namespace SpaceEngineers.Core.Roslyn.Test.Implementations +{ + using System; + using System.Collections.Generic; + using System.IO; + using System.Linq; + using Abstractions; + using AutoRegistration.Api.Abstractions; + using AutoRegistration.Api.Attributes; + using AutoRegistration.Api.Enumerations; + using Basics; + using Basics.Exceptions; + using CompositionRoot; + using Extensions; + using Microsoft.CodeAnalysis.CodeFixes; + using Microsoft.CodeAnalysis.Diagnostics; + using Microsoft.CodeAnalysis.Text; + using ValueObjects; + + /// + /// ConventionalProvider + /// + [Component(EnLifestyle.Singleton)] + internal class ConventionalProvider : IConventionalProvider, + IResolvable + { + private readonly IDependencyContainer _dependencyContainer; + + private readonly IEnumerable _transformers; + + /// .cctor + /// IDependencyContainer + /// ISourceTransformer + public ConventionalProvider(IDependencyContainer dependencyContainer, IEnumerable transformers) + { + _dependencyContainer = dependencyContainer; + _transformers = transformers; + } + + private static Func AnalyzerTypeName => analyzer => analyzer.GetType().Name; + + /// + public CodeFixProvider? CodeFixProvider(DiagnosticAnalyzer analyzer) + { + var analyzerTypeName = AnalyzerTypeName(analyzer); + var count = analyzerTypeName.Length - Conventions.Analyzer.Length; + var codeFixProviderTypeName = analyzerTypeName.Substring(0, count) + Conventions.CodeFix; + + return _dependencyContainer + .ResolveCollection() + .SingleOrDefault(c => c.GetType().Name == codeFixProviderTypeName); + } + + /// + public IExpectedDiagnosticsProvider ExpectedDiagnosticsProvider(DiagnosticAnalyzer analyzer) + { + var analyzerTypeName = AnalyzerTypeName(analyzer); + var providerTypeName = analyzerTypeName + Conventions.ExpectedDiagnosticsProviderSuffix; + + var providerType = _dependencyContainer + .Resolve() + .OurTypes + .SingleOrDefault(t => t.Name == providerTypeName) + ?? throw new NotFoundException($"Provide {nameof(ExpectedDiagnosticsProvider)} for {analyzerTypeName} or place it in directory different from source directory"); + + return (IExpectedDiagnosticsProvider)_dependencyContainer.Resolve(providerType); + } + + /// + public IEnumerable SourceFiles(DiagnosticAnalyzer analyzer, string? directorySuffix = null) + { + var projectFileDirectory = SolutionExtensions.ProjectFile().Directory + ?? throw new InvalidOperationException($"Project directory {nameof(Roslyn)}.{nameof(Test)} wasn't found"); + + return projectFileDirectory + .StepInto(Conventions.SourceDirectory) + .StepInto(analyzer.GetType().Name + (directorySuffix ?? string.Empty)) + .GetFiles("*" + AnalysisExtensions.CSharpDefaultFileExt, SearchOption.TopDirectoryOnly) + .Select(file => + { + using var stream = file.OpenRead(); + return (file.NameWithoutExtension(), SourceText.From(stream)); + }) + .Select(tuple => + { + var (name, text) = tuple; + return new SourceFile(name, _transformers.Aggregate(text, (acc, next) => next.Transform(acc))); + }); + } + } +} \ No newline at end of file diff --git a/tests/Tests/Roslyn.Test/Implementations/Conventions.cs b/tests/Tests/Roslyn.Test/Implementations/Conventions.cs new file mode 100644 index 00000000..87a967d9 --- /dev/null +++ b/tests/Tests/Roslyn.Test/Implementations/Conventions.cs @@ -0,0 +1,17 @@ +namespace SpaceEngineers.Core.Roslyn.Test.Implementations +{ + internal class Conventions + { + internal const string Analyzer = "Analyzer"; + + internal const string CodeFix = "CodeFix"; + + internal const string Expected = "Expected"; + + internal const string NameAttribute = "Name"; + + internal const string SourceDirectory = "Sources"; + + internal const string ExpectedDiagnosticsProviderSuffix = "ExpectedDiagnosticsProvider"; + } +} \ No newline at end of file diff --git a/tests/Tests/Roslyn.Test/Implementations/DiagnosticAnalyzerVerifier.cs b/tests/Tests/Roslyn.Test/Implementations/DiagnosticAnalyzerVerifier.cs new file mode 100644 index 00000000..1b728e71 --- /dev/null +++ b/tests/Tests/Roslyn.Test/Implementations/DiagnosticAnalyzerVerifier.cs @@ -0,0 +1,160 @@ +namespace SpaceEngineers.Core.Roslyn.Test.Implementations +{ + using System; + using System.Collections.Immutable; + using System.Globalization; + using System.Linq; + using System.Text; + using Abstractions; + using Analyzers.Api; + using AutoRegistration.Api.Abstractions; + using AutoRegistration.Api.Attributes; + using AutoRegistration.Api.Enumerations; + using Microsoft.CodeAnalysis; + using Microsoft.CodeAnalysis.Diagnostics; + using ValueObjects; + using Xunit; + + [Component(EnLifestyle.Singleton)] + internal class DiagnosticAnalyzerVerifier : IDiagnosticAnalyzerVerifier, + IResolvable + { + public void VerifyAnalyzedDocument(SyntaxAnalyzerBase analyzer, + AnalyzedDocument analyzedDocument, + ImmutableArray expectedDiagnostics) + { + VerifyDiagnostics(analyzer, analyzedDocument.ActualDiagnostics.ToArray(), expectedDiagnostics.ToArray()); + } + + private static void VerifyDiagnostics(SyntaxAnalyzerBase analyzer, + Diagnostic[] actualDiagnostics, + params ExpectedDiagnostic[] expectedResults) + { + var expectedCount = expectedResults.Length; + var actualCount = actualDiagnostics.Length; + + if (expectedCount != actualCount) + { + var diagnosticsOutput = actualDiagnostics.Any() ? FormatDiagnostics(analyzer, actualDiagnostics.ToArray()) : " NONE."; + + Assert.Fail($"Mismatch between number of diagnostics returned, expected \"{expectedCount}\" actual \"{actualCount}\"{Environment.NewLine}Diagnostics:{Environment.NewLine}{diagnosticsOutput}{Environment.NewLine}"); + } + + for (var i = 0; i < expectedResults.Length; i++) + { + var actual = actualDiagnostics[i]; + var expected = expectedResults[i]; + + if (expected.Location.Line == -1 && expected.Location.Column == -1) + { + if (actual.Location != Location.None) + { + Assert.Fail($"Expected:{Environment.NewLine}A project diagnostic with No location{Environment.NewLine}Actual:{Environment.NewLine}{FormatDiagnostics(analyzer, actual)}"); + } + } + else + { + VerifyDiagnosticLocation(analyzer, actual, actual.Location, expected.Location); + } + + if (actual.Id != expected.Descriptor.Id) + { + Assert.Fail($"Expected diagnostic id to be \"{expected.Descriptor.Id}\" was \"{actual.Id}\"{Environment.NewLine}Diagnostic:{Environment.NewLine} {FormatDiagnostics(analyzer, actual)}{Environment.NewLine}"); + } + + if (actual.Severity != expected.Severity) + { + Assert.Fail($"Expected diagnostic severity to be \"{expected.Severity}\" was \"{actual.Severity}\"{Environment.NewLine}Diagnostic:{Environment.NewLine} {FormatDiagnostics(analyzer, actual)}{Environment.NewLine}"); + } + + if (actual.GetMessage(CultureInfo.InvariantCulture) != expected.ActualMessage) + { + Assert.Fail($"Expected diagnostic message to be \"{expected.ActualMessage}\" was \"{actual.GetMessage(CultureInfo.InvariantCulture)}\"{Environment.NewLine}Diagnostic:{Environment.NewLine} {FormatDiagnostics(analyzer, actual)}{Environment.NewLine}"); + } + } + } + + private static void VerifyDiagnosticLocation(DiagnosticAnalyzer analyzer, Diagnostic diagnostic, Location actual, DiagnosticLocation expected) + { + var actualSpan = actual.GetLineSpan(); + + Assert.True(actualSpan.Path == expected.SourceFile + || (actualSpan.Path != null + && actualSpan.Path.Contains(expected.SourceFile, StringComparison.Ordinal)), + $"Expected diagnostic to be in file \"{expected.SourceFile}\" was actually in file \"{actualSpan.Path}\"{Environment.NewLine}Diagnostic:{Environment.NewLine} {FormatDiagnostics(analyzer, diagnostic)}{Environment.NewLine}"); + + var actualLinePosition = actualSpan.StartLinePosition; + + // Only check line position if there is an actual line in the real diagnostic + if (actualLinePosition.Line > 0) + { + if (actualLinePosition.Line + 1 != expected.Line) + { + Assert.Fail($"Expected diagnostic to be on line \"{expected.Line}\" was actually on line \"{actualLinePosition.Line + 1}\"{Environment.NewLine}Diagnostic:{Environment.NewLine} {FormatDiagnostics(analyzer, diagnostic)}{Environment.NewLine}"); + } + } + + // Only check column position if there is an actual column position in the real diagnostic + if (actualLinePosition.Character > 0) + { + if (actualLinePosition.Character + 1 != expected.Column) + { + Assert.Fail($"Expected diagnostic to start at column \"{expected.Column}\" was actually at column \"{actualLinePosition.Character + 1}\"{Environment.NewLine}Diagnostic:{Environment.NewLine} {FormatDiagnostics(analyzer, diagnostic)}{Environment.NewLine}"); + } + } + } + + private static string FormatDiagnostics(DiagnosticAnalyzer analyzer, params Diagnostic[] diagnostics) + { + var builder = new StringBuilder(); + for (var i = 0; i < diagnostics.Length; ++i) + { + builder.AppendLine("// " + diagnostics[i]); + + var analyzerType = analyzer.GetType(); + var rules = analyzer.SupportedDiagnostics; + + foreach (var rule in rules) + { + if (rule != null && rule.Id == diagnostics[i].Id) + { + var location = diagnostics[i].Location; + if (location == Location.None) + { + builder.AppendFormat(CultureInfo.InvariantCulture, "GetGlobalResult({0}.{1})", analyzerType.Name, rule.Id); + } + else + { + Assert.True(location.IsInSource, + $"Test base does not currently handle diagnostics in metadata locations. Diagnostic in metadata: {diagnostics[i]}{Environment.NewLine}"); + + var resultMethodName = diagnostics[i].Location.SourceTree.FilePath.EndsWith(".cs", StringComparison.Ordinal) + ? "expected((" + : throw new InvalidOperationException("Choose another language from C#"); + var linePosition = diagnostics[i].Location.GetLineSpan().StartLinePosition; + + builder.AppendFormat(CultureInfo.InvariantCulture, + "{0}({1}, {2}, {3}.{4}, \"{5}\"))", + resultMethodName, + linePosition.Line + 1, + linePosition.Character + 1, + nameof(DiagnosticSeverity), + rule.DefaultSeverity, + rule.MessageFormat); + } + + if (i != diagnostics.Length - 1) + { + builder.Append(','); + } + + builder.AppendLine(); + break; + } + } + } + + return builder.ToString(); + } + } +} diff --git a/tests/Tests/Roslyn.Test/Implementations/ExpectedDiagnosticsProviderBase.cs b/tests/Tests/Roslyn.Test/Implementations/ExpectedDiagnosticsProviderBase.cs new file mode 100644 index 00000000..a1aa0b74 --- /dev/null +++ b/tests/Tests/Roslyn.Test/Implementations/ExpectedDiagnosticsProviderBase.cs @@ -0,0 +1,33 @@ +namespace SpaceEngineers.Core.Roslyn.Test.Implementations +{ + using System; + using System.Collections.Generic; + using System.Collections.Immutable; + using System.Linq; + using Abstractions; + using Analyzers.Api; + using Microsoft.CodeAnalysis; + using ValueObjects; + + internal abstract class ExpectedDiagnosticsProviderBase : IExpectedDiagnosticsProvider + { + public IDictionary> ByFileName(SyntaxAnalyzerBase analyzer) + { + return ExpectedInternal(analyzer, Compose(analyzer)); + } + + protected abstract IDictionary> ExpectedInternal( + SyntaxAnalyzerBase analyzer, + Func<(string, int, int, DiagnosticSeverity, string), ExpectedDiagnostic> expected); + + private static Func<(string, int, int, DiagnosticSeverity, string), ExpectedDiagnostic> Compose(SyntaxAnalyzerBase analyzer) + { + return args => + { + var (sourceFileName, line, column, severity, msg) = args; + return new ExpectedDiagnostic(analyzer.SupportedDiagnostics.Single(), msg, severity) + .WithLocation(sourceFileName, line, column); + }; + } + } +} \ No newline at end of file diff --git a/tests/Tests/Roslyn.Test/MetadataReferenceProviders/CompositionRootMetadataReferenceProvider.cs b/tests/Tests/Roslyn.Test/MetadataReferenceProviders/CompositionRootMetadataReferenceProvider.cs new file mode 100644 index 00000000..3afe1327 --- /dev/null +++ b/tests/Tests/Roslyn.Test/MetadataReferenceProviders/CompositionRootMetadataReferenceProvider.cs @@ -0,0 +1,26 @@ +namespace SpaceEngineers.Core.Roslyn.Test.MetadataReferenceProviders +{ + using System.Collections.Generic; + using Abstractions; + using AutoRegistration.Api.Attributes; + using AutoRegistration.Api.Enumerations; + using Basics; + using Extensions; + using Microsoft.CodeAnalysis; + + /// + [Component(EnLifestyle.Singleton)] + internal class CompositionRootMetadataReferenceProvider : IMetadataReferenceProvider + { + /// + public IEnumerable ReceiveReferences() + { + return new[] + { + AssembliesExtensions.FindRequiredAssembly(AssembliesExtensions.BuildName(nameof(SpaceEngineers), nameof(Core), nameof(Basics))).AsMetadataReference(), + AssembliesExtensions.FindRequiredAssembly(AssembliesExtensions.BuildName(nameof(SpaceEngineers), nameof(Core), nameof(AutoRegistration), nameof(AutoRegistration.Api))).AsMetadataReference(), + AssembliesExtensions.FindRequiredAssembly(AssembliesExtensions.BuildName(nameof(SpaceEngineers), nameof(Core), nameof(CompositionRoot))).AsMetadataReference() + }; + } + } +} \ No newline at end of file diff --git a/tests/Tests/Roslyn.Test/MetadataReferenceProviders/DotNetSdkMetadataReferenceProvider.cs b/tests/Tests/Roslyn.Test/MetadataReferenceProviders/DotNetSdkMetadataReferenceProvider.cs new file mode 100644 index 00000000..9a4f8850 --- /dev/null +++ b/tests/Tests/Roslyn.Test/MetadataReferenceProviders/DotNetSdkMetadataReferenceProvider.cs @@ -0,0 +1,33 @@ +namespace SpaceEngineers.Core.Roslyn.Test.MetadataReferenceProviders +{ + using System; + using System.Collections.Generic; + using Abstractions; + using AutoRegistration.Api.Attributes; + using AutoRegistration.Api.Enumerations; + using Basics; + using Extensions; + using Microsoft.CodeAnalysis; + + /// + [Component(EnLifestyle.Singleton)] + internal class DotNetSdkMetadataReferenceProvider : IMetadataReferenceProvider + { + /// + public IEnumerable ReceiveReferences() + { + var frameworkDirectory = typeof(object) + .Assembly + .Location + .AsFileInfo() + .Directory + ?? throw new InvalidOperationException(".NET Framework directory wasn't found"); + + yield return frameworkDirectory.GetFile("netstandard", ".dll").AsMetadataReference(); + yield return frameworkDirectory.GetFile("mscorlib", ".dll").AsMetadataReference(); + yield return frameworkDirectory.GetFile("System.Private.CoreLib", ".dll").AsMetadataReference(); + yield return frameworkDirectory.GetFile("System.Runtime", ".dll").AsMetadataReference(); + yield return frameworkDirectory.GetFile("System.Reflection", ".dll").AsMetadataReference(); + } + } +} \ No newline at end of file diff --git a/tests/Tests/Roslyn.Test/Properties/AssemblyInfo.cs b/tests/Tests/Roslyn.Test/Properties/AssemblyInfo.cs new file mode 100644 index 00000000..31adb06d --- /dev/null +++ b/tests/Tests/Roslyn.Test/Properties/AssemblyInfo.cs @@ -0,0 +1,5 @@ +using System.Reflection; + +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] +[assembly: AssemblyInformationalVersion("1.0.0.0")] \ No newline at end of file diff --git a/tests/Tests/Roslyn.Test/Registrations/AnalyzersManualRegistration.cs b/tests/Tests/Roslyn.Test/Registrations/AnalyzersManualRegistration.cs new file mode 100644 index 00000000..925507fe --- /dev/null +++ b/tests/Tests/Roslyn.Test/Registrations/AnalyzersManualRegistration.cs @@ -0,0 +1,27 @@ +namespace SpaceEngineers.Core.Roslyn.Test.Registrations +{ + using System.Linq; + using AutoRegistration.Api.Enumerations; + using Basics; + using CompositionRoot.Registration; + using Microsoft.CodeAnalysis.CodeFixes; + using Microsoft.CodeAnalysis.Diagnostics; + + internal class AnalyzersManualRegistration : IManualRegistration + { + public void Register(IManualRegistrationsContainer container) + { + container + .Types + .OurTypes + .Where(type => typeof(DiagnosticAnalyzer).IsAssignableFrom(type) && type.IsConcreteType()) + .Each(implementation => container.RegisterCollectionEntry(implementation, EnLifestyle.Singleton)); + + container + .Types + .OurTypes + .Where(type => typeof(CodeFixProvider).IsAssignableFrom(type) && type.IsConcreteType()) + .Each(implementation => container.RegisterCollectionEntry(implementation, EnLifestyle.Singleton)); + } + } +} \ No newline at end of file diff --git a/tests/Tests/Roslyn.Test/Roslyn.Test.csproj b/tests/Tests/Roslyn.Test/Roslyn.Test.csproj new file mode 100644 index 00000000..78d4db9c --- /dev/null +++ b/tests/Tests/Roslyn.Test/Roslyn.Test.csproj @@ -0,0 +1,49 @@ + + + + net7.0 + SpaceEngineers.Core.Roslyn.Test + SpaceEngineers.Core.Roslyn.Test + false + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + runtime + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + AnalysisBase.cs + + + AnalysisBase.cs + + + \ No newline at end of file diff --git a/tests/Tests/Roslyn.Test/Settings/appsettings.json b/tests/Tests/Roslyn.Test/Settings/appsettings.json new file mode 100644 index 00000000..27bbd500 --- /dev/null +++ b/tests/Tests/Roslyn.Test/Settings/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Debug", + "System": "Information", + "Microsoft": "Information" + } + } +} \ No newline at end of file diff --git a/tests/Tests/Roslyn.Test/Sources/ComponentAttributeAnalyzer/EmptyAttributesListSource.cs b/tests/Tests/Roslyn.Test/Sources/ComponentAttributeAnalyzer/EmptyAttributesListSource.cs new file mode 100644 index 00000000..3ff248a8 --- /dev/null +++ b/tests/Tests/Roslyn.Test/Sources/ComponentAttributeAnalyzer/EmptyAttributesListSource.cs @@ -0,0 +1,8 @@ +namespace SpaceEngineers.Core.Roslyn.Test.Sources.ComponentAttributeAnalyzer +{ + using AutoRegistration.Api.Abstractions; + + internal class EmptyAttributesListSource : ITestService, IResolvable + { + } +} \ No newline at end of file diff --git a/tests/Tests/Roslyn.Test/Sources/ComponentAttributeAnalyzer/ExistedAttributeSource.cs b/tests/Tests/Roslyn.Test/Sources/ComponentAttributeAnalyzer/ExistedAttributeSource.cs new file mode 100644 index 00000000..2eff79cc --- /dev/null +++ b/tests/Tests/Roslyn.Test/Sources/ComponentAttributeAnalyzer/ExistedAttributeSource.cs @@ -0,0 +1,13 @@ +namespace SpaceEngineers.Core.Roslyn.Test.Sources.ComponentAttributeAnalyzer +{ + using System; + using AutoRegistration.Api.Abstractions; + using AutoRegistration.Api.Attributes; + using AutoRegistration.Api.Enumerations; + + [Component(EnLifestyle.Singleton)] + [Serializable] + internal class ExistedAttributeSource : ITestService, IResolvable + { + } +} \ No newline at end of file diff --git a/tests/Tests/Roslyn.Test/Sources/ComponentAttributeAnalyzer/FixWithLeadingTriviaAndAttributesSource.cs b/tests/Tests/Roslyn.Test/Sources/ComponentAttributeAnalyzer/FixWithLeadingTriviaAndAttributesSource.cs new file mode 100644 index 00000000..72bc8d04 --- /dev/null +++ b/tests/Tests/Roslyn.Test/Sources/ComponentAttributeAnalyzer/FixWithLeadingTriviaAndAttributesSource.cs @@ -0,0 +1,13 @@ +namespace SpaceEngineers.Core.Roslyn.Test.Sources.ComponentAttributeAnalyzer +{ + using System; + using AutoRegistration.Api.Abstractions; + + /// + /// Summary + /// + [Serializable] + internal class FixWithLeadingTriviaAndAttributesSource : ITestService, IResolvable + { + } +} \ No newline at end of file diff --git a/tests/Tests/Roslyn.Test/Sources/ComponentAttributeAnalyzer/FixWithLeadingTriviaSource.cs b/tests/Tests/Roslyn.Test/Sources/ComponentAttributeAnalyzer/FixWithLeadingTriviaSource.cs new file mode 100644 index 00000000..4ccb28f0 --- /dev/null +++ b/tests/Tests/Roslyn.Test/Sources/ComponentAttributeAnalyzer/FixWithLeadingTriviaSource.cs @@ -0,0 +1,11 @@ +namespace SpaceEngineers.Core.Roslyn.Test.Sources.ComponentAttributeAnalyzer +{ + using AutoRegistration.Api.Abstractions; + + /// + /// Summary + /// + internal class FixWithLeadingTriviaSource : ITestService, IResolvable + { + } +} \ No newline at end of file diff --git a/tests/Tests/Roslyn.Test/Sources/ComponentAttributeAnalyzer/FixWithoutLeadingTriviaSource.cs b/tests/Tests/Roslyn.Test/Sources/ComponentAttributeAnalyzer/FixWithoutLeadingTriviaSource.cs new file mode 100644 index 00000000..c8ca4583 --- /dev/null +++ b/tests/Tests/Roslyn.Test/Sources/ComponentAttributeAnalyzer/FixWithoutLeadingTriviaSource.cs @@ -0,0 +1,8 @@ +namespace SpaceEngineers.Core.Roslyn.Test.Sources.ComponentAttributeAnalyzer +{ + using AutoRegistration.Api.Abstractions; + + internal class FixWithoutLeadingTriviaSource : ITestService, IResolvable + { + } +} \ No newline at end of file diff --git a/tests/Tests/Roslyn.Test/Sources/ComponentAttributeAnalyzer/ITestCollectionResolvableService.cs b/tests/Tests/Roslyn.Test/Sources/ComponentAttributeAnalyzer/ITestCollectionResolvableService.cs new file mode 100644 index 00000000..ada54a3a --- /dev/null +++ b/tests/Tests/Roslyn.Test/Sources/ComponentAttributeAnalyzer/ITestCollectionResolvableService.cs @@ -0,0 +1,6 @@ +namespace SpaceEngineers.Core.Roslyn.Test.Sources.ComponentAttributeAnalyzer +{ + internal interface ITestCollectionResolvableService + { + } +} \ No newline at end of file diff --git a/tests/Tests/Roslyn.Test/Sources/ComponentAttributeAnalyzer/NotEmptyAttributesListSource.cs b/tests/Tests/Roslyn.Test/Sources/ComponentAttributeAnalyzer/NotEmptyAttributesListSource.cs new file mode 100644 index 00000000..43e1378c --- /dev/null +++ b/tests/Tests/Roslyn.Test/Sources/ComponentAttributeAnalyzer/NotEmptyAttributesListSource.cs @@ -0,0 +1,10 @@ +namespace SpaceEngineers.Core.Roslyn.Test.Sources.ComponentAttributeAnalyzer +{ + using System; + using AutoRegistration.Api.Abstractions; + + [Serializable] + internal class NotEmptyAttributesListSource : ITestCollectionResolvableService, ICollectionResolvable + { + } +} \ No newline at end of file diff --git a/tests/Tests/Roslyn.Test/Sources/ComponentAttributeAnalyzerExpected/FixWithLeadingTriviaAndAttributesSourceExpected.cs b/tests/Tests/Roslyn.Test/Sources/ComponentAttributeAnalyzerExpected/FixWithLeadingTriviaAndAttributesSourceExpected.cs new file mode 100644 index 00000000..1f7427c6 --- /dev/null +++ b/tests/Tests/Roslyn.Test/Sources/ComponentAttributeAnalyzerExpected/FixWithLeadingTriviaAndAttributesSourceExpected.cs @@ -0,0 +1,16 @@ +namespace SpaceEngineers.Core.Roslyn.Test.Sources.ComponentAttributeAnalyzerExpected +{ + using System; + using AutoRegistration.Api.Abstractions; + using SpaceEngineers.Core.AutoRegistration.Api.Attributes; + using SpaceEngineers.Core.AutoRegistration.Api.Enumerations; + + /// + /// Summary + /// + [Serializable] + /*[Component(EnLifestyle.ChooseLifestyle)]*/ + internal class FixWithLeadingTriviaAndAttributesSourceExpected : ITestService, IResolvable + { + } +} \ No newline at end of file diff --git a/tests/Tests/Roslyn.Test/Sources/ComponentAttributeAnalyzerExpected/FixWithLeadingTriviaSourceExpected.cs b/tests/Tests/Roslyn.Test/Sources/ComponentAttributeAnalyzerExpected/FixWithLeadingTriviaSourceExpected.cs new file mode 100644 index 00000000..c42fe868 --- /dev/null +++ b/tests/Tests/Roslyn.Test/Sources/ComponentAttributeAnalyzerExpected/FixWithLeadingTriviaSourceExpected.cs @@ -0,0 +1,14 @@ +namespace SpaceEngineers.Core.Roslyn.Test.Sources.ComponentAttributeAnalyzerExpected +{ + using AutoRegistration.Api.Abstractions; + using SpaceEngineers.Core.AutoRegistration.Api.Attributes; + using SpaceEngineers.Core.AutoRegistration.Api.Enumerations; + + /// + /// Summary + /// + /*[Component(EnLifestyle.ChooseLifestyle)]*/ + internal class FixWithLeadingTriviaSourceExpected : ITestService, IResolvable + { + } +} \ No newline at end of file diff --git a/tests/Tests/Roslyn.Test/Sources/ComponentAttributeAnalyzerExpected/FixWithoutLeadingTriviaSourceExpected.cs b/tests/Tests/Roslyn.Test/Sources/ComponentAttributeAnalyzerExpected/FixWithoutLeadingTriviaSourceExpected.cs new file mode 100644 index 00000000..68bdd4ad --- /dev/null +++ b/tests/Tests/Roslyn.Test/Sources/ComponentAttributeAnalyzerExpected/FixWithoutLeadingTriviaSourceExpected.cs @@ -0,0 +1,11 @@ +namespace SpaceEngineers.Core.Roslyn.Test.Sources.ComponentAttributeAnalyzerExpected +{ + using AutoRegistration.Api.Abstractions; + using SpaceEngineers.Core.AutoRegistration.Api.Attributes; + using SpaceEngineers.Core.AutoRegistration.Api.Enumerations; + + /*[Component(EnLifestyle.ChooseLifestyle)]*/ + internal class FixWithoutLeadingTriviaSourceExpected : ITestService, IResolvable + { + } +} \ No newline at end of file diff --git a/tests/Tests/Roslyn.Test/Sources/ITestService.cs b/tests/Tests/Roslyn.Test/Sources/ITestService.cs new file mode 100644 index 00000000..eefcf92e --- /dev/null +++ b/tests/Tests/Roslyn.Test/Sources/ITestService.cs @@ -0,0 +1,9 @@ +namespace SpaceEngineers.Core.Roslyn.Test.Sources +{ + /// + /// ITestService + /// + public interface ITestService + { + } +} \ No newline at end of file diff --git a/tests/Tests/Roslyn.Test/Tests/Analysis.cs b/tests/Tests/Roslyn.Test/Tests/Analysis.cs new file mode 100644 index 00000000..4081f45d --- /dev/null +++ b/tests/Tests/Roslyn.Test/Tests/Analysis.cs @@ -0,0 +1,142 @@ +namespace SpaceEngineers.Core.Roslyn.Test.Tests +{ + using System; + using System.Collections.Generic; + using System.Collections.Immutable; + using System.Diagnostics.CodeAnalysis; + using System.Linq; + using System.Threading; + using System.Threading.Tasks; + using Abstractions; + using Analyzers.Api; + using Basics; + using Basics.Exceptions; + using Core.Test.Api.ClassFixtures; + using Extensions; + using Implementations; + using Microsoft.CodeAnalysis; + using Microsoft.CodeAnalysis.CodeFixes; + using Microsoft.CodeAnalysis.Diagnostics; + using Xunit; + using Xunit.Abstractions; + + /// + /// AnalyzerTest + /// + [Collection(nameof(Analysis))] + public class Analysis : AnalysisBase + { + private static readonly ImmutableArray IgnoredProjects + = ImmutableArray.Empty; + + private static readonly ImmutableArray IgnoredSources + = ImmutableArray.Create(nameof(IExpectedDiagnosticsProvider).Substring(1, nameof(IExpectedDiagnosticsProvider).Length - 1)); + + /// .cctor + /// ITestOutputHelper + /// TestFixture + public Analysis(ITestOutputHelper output, TestFixture fixture) + : base(output, fixture) + { + } + + /// + protected override ImmutableArray IgnoredNamespaces => + ImmutableArray.Create("SpaceEngineers.Core.Roslyn.Test.Sources", + "SpaceEngineers.Core.Roslyn.Test.Sources.ComponentAttributeAnalyzer", + "SpaceEngineers.Core.Roslyn.Test.Sources.ComponentAttributeAnalyzerExpected"); + + [Fact] + internal async Task AnalysisTest() + { + var analyzers = DependencyContainer + .ResolveCollection() + .OfType(); + + foreach (var analyzer in analyzers) + { + var conventionalProvider = DependencyContainer.Resolve(); + var codeFixProvider = conventionalProvider.CodeFixProvider(analyzer); + + await TestSingleAnalyzer(analyzer, codeFixProvider, conventionalProvider).ConfigureAwait(false); + } + } + + [SuppressMessage("Analysis", "CA1506", Justification = "application composition root")] + private async Task TestSingleAnalyzer(SyntaxAnalyzerBase analyzer, + CodeFixProvider? codeFix, + IConventionalProvider conventionalProvider) + { + var analyzerVerifier = DependencyContainer.Resolve(); + var codeFixVerifier = DependencyContainer.Resolve(); + var metadataReferences = DependencyContainer + .ResolveCollection() + .SelectMany(p => p.ReceiveReferences()) + .Distinct() + .ToArray(); + + using (var workspace = new AdhocWorkspace()) + { + workspace.WorkspaceFailed += (sender, workspaceFailedArgs) => + { + Output.WriteLine(workspaceFailedArgs.Diagnostic.Message); + }; + + var analyzerSources = conventionalProvider.SourceFiles(analyzer); + var diagnostics = workspace + .CurrentSolution + .SetupSolution(GetType().Name, analyzerSources, metadataReferences) + .CompileSolution(ImmutableArray.Create((DiagnosticAnalyzer)analyzer), + IgnoredProjects, + IgnoredSources, + ImmutableArray.Empty); + + var expectedDiagnostics = conventionalProvider + .ExpectedDiagnosticsProvider(analyzer) + .ByFileName(analyzer); + var expectedFixedSources = conventionalProvider + .SourceFiles(analyzer, "Expected") + .ToDictionary(s => s.Name); + + await foreach (var analyzedDocument in diagnostics + .WithCancellation(CancellationToken.None) + .ConfigureAwait(false)) + { + foreach (var diagnostic in analyzedDocument.ActualDiagnostics) + { + Output.WriteLine(diagnostic.ToString()); + } + + if (!expectedDiagnostics.Remove(analyzedDocument.Name, out var expected)) + { + throw new InvalidOperationException($"Unsupported source file: {analyzedDocument.Name}"); + } + + analyzerVerifier.VerifyAnalyzedDocument(analyzer, analyzedDocument, expected); + + if (codeFix != null + && expectedFixedSources.Remove(analyzedDocument.Name + Conventions.Expected, out var expectedSource)) + { + await codeFixVerifier.VerifyCodeFix(analyzer, codeFix, analyzedDocument, expectedSource, Output.WriteLine).ConfigureAwait(false); + } + } + + if (expectedDiagnostics.Any()) + { + var files = expectedDiagnostics.Keys.ToString(", "); + throw new InvalidOperationException($"Ambiguous diagnostics in files: {files}"); + } + + if (expectedFixedSources.Any()) + { + if (codeFix == null) + { + throw new NotFoundException($"Specify code fix for: {analyzer.GetType().Name}"); + } + + throw new InvalidOperationException($"Ambiguous expected codeFix sources: {expectedFixedSources.Keys.ToString(", ")}"); + } + } + } + } +} \ No newline at end of file diff --git a/tests/Tests/Roslyn.Test/Tests/AnalysisBase.cs b/tests/Tests/Roslyn.Test/Tests/AnalysisBase.cs new file mode 100644 index 00000000..168430f3 --- /dev/null +++ b/tests/Tests/Roslyn.Test/Tests/AnalysisBase.cs @@ -0,0 +1,83 @@ +namespace SpaceEngineers.Core.Roslyn.Test.Tests +{ + using System; + using System.Collections.Immutable; + using System.Diagnostics.CodeAnalysis; + using System.Linq; + using Basics; + using CompositionRoot; + using Core.Test.Api; + using Core.Test.Api.ClassFixtures; + using CrossCuttingConcerns.Settings; + using Microsoft.Build.Locator; + using Registrations; + using Xunit.Abstractions; + + /// + /// AnalysisBase + /// + public abstract class AnalysisBase : TestBase + { + private static readonly Version[] AvailableVersions; + private static readonly Version? Version; + + [SuppressMessage("Analysis", "CA1810", Justification = "Analysis test")] + static AnalysisBase() + { + AvailableVersions = MSBuildLocator + .QueryVisualStudioInstances() + .Select(it => it.Version) + .OrderByDescending(it => it) + .ToArray(); + + var instance = MSBuildLocator + .QueryVisualStudioInstances() + .OrderByDescending(it => it.Version) + .First(); + + MSBuildLocator.RegisterInstance(instance); + + Version = instance.Version; + } + + /// .cctor + /// ITestOutputHelper + /// TestFixture + protected AnalysisBase(ITestOutputHelper output, TestFixture fixture) + : base(output, fixture) + { + output.WriteLine($"Used framework version: {Version}"); + output.WriteLine($"Available versions: {AvailableVersions.Select(v => v.ToString()).ToString(", ")}"); + + var projectFileDirectory = SolutionExtensions.ProjectFile().Directory + ?? throw new InvalidOperationException("Project directory wasn't found"); + + var settingsDirectory = projectFileDirectory.StepInto("Settings"); + + var assemblies = new[] + { + AssembliesExtensions.FindRequiredAssembly(AssembliesExtensions.BuildName(nameof(SpaceEngineers), nameof(Core), nameof(Analyzers), nameof(Analyzers.Api))), + AssembliesExtensions.FindRequiredAssembly(AssembliesExtensions.BuildName(nameof(SpaceEngineers), nameof(Core), nameof(AutoRegistration), nameof(AutoRegistration.Api), nameof(AutoRegistration.Api.Analyzers))), + AssembliesExtensions.FindRequiredAssembly(AssembliesExtensions.BuildName(nameof(SpaceEngineers), nameof(Core), nameof(Roslyn), nameof(Test))) + }; + + var options = new DependencyContainerOptions() + .WithPluginAssemblies(assemblies) + .WithManualRegistrations(new AnalyzersManualRegistration()) + .WithManualRegistrations(new SettingsDirectoryProviderManualRegistration(new SettingsDirectoryProvider(settingsDirectory))) + .WithExcludedNamespaces(IgnoredNamespaces.ToArray()); + + DependencyContainer = fixture.DependencyContainer(options); + } + + /// + /// Excluded namespaces + /// + protected abstract ImmutableArray IgnoredNamespaces { get; } + + /// + /// IDependencyContainer + /// + protected IDependencyContainer DependencyContainer { get; } + } +} \ No newline at end of file diff --git a/tests/Tests/Roslyn.Test/Tests/ComponentAttributeAnalyzerExpectedDiagnosticsProvider.cs b/tests/Tests/Roslyn.Test/Tests/ComponentAttributeAnalyzerExpectedDiagnosticsProvider.cs new file mode 100644 index 00000000..9818b409 --- /dev/null +++ b/tests/Tests/Roslyn.Test/Tests/ComponentAttributeAnalyzerExpectedDiagnosticsProvider.cs @@ -0,0 +1,38 @@ +namespace SpaceEngineers.Core.Roslyn.Test.Tests +{ + using System; + using System.Collections.Generic; + using System.Collections.Immutable; + using Abstractions; + using Analyzers.Api; + using AutoRegistration.Api.Abstractions; + using AutoRegistration.Api.Analyzers; + using AutoRegistration.Api.Attributes; + using AutoRegistration.Api.Enumerations; + using Implementations; + using Microsoft.CodeAnalysis; + using Sources.ComponentAttributeAnalyzer; + using ValueObjects; + + [Component(EnLifestyle.Singleton)] + internal class ComponentAttributeAnalyzerExpectedDiagnosticsProvider : ExpectedDiagnosticsProviderBase, + IResolvable, + IResolvable + { + protected override IDictionary> ExpectedInternal( + SyntaxAnalyzerBase analyzer, + Func<(string, int, int, DiagnosticSeverity, string), ExpectedDiagnostic> expected) + { + var a = (ComponentAttributeAnalyzer)analyzer; + + return new Dictionary> + { + [nameof(EmptyAttributesListSource)] = ImmutableArray.Create(expected((nameof(EmptyAttributesListSource), 5, 20, DiagnosticSeverity.Error, a.MarkWithComponentAttribute))), + [nameof(FixWithLeadingTriviaSource)] = ImmutableArray.Create(expected((nameof(FixWithLeadingTriviaSource), 8, 20, DiagnosticSeverity.Error, a.MarkWithComponentAttribute))), + [nameof(FixWithoutLeadingTriviaSource)] = ImmutableArray.Create(expected((nameof(FixWithoutLeadingTriviaSource), 5, 20, DiagnosticSeverity.Error, a.MarkWithComponentAttribute))), + [nameof(FixWithLeadingTriviaAndAttributesSource)] = ImmutableArray.Create(expected((nameof(FixWithLeadingTriviaAndAttributesSource), 10, 20, DiagnosticSeverity.Error, a.MarkWithComponentAttribute))), + [nameof(NotEmptyAttributesListSource)] = ImmutableArray.Create(expected((nameof(NotEmptyAttributesListSource), 7, 20, DiagnosticSeverity.Error, a.MarkWithComponentAttribute))) + }; + } + } +} \ No newline at end of file diff --git a/tests/Tests/Roslyn.Test/Tests/DisplayNameTestCollectionOrderer.cs b/tests/Tests/Roslyn.Test/Tests/DisplayNameTestCollectionOrderer.cs new file mode 100644 index 00000000..46474ec7 --- /dev/null +++ b/tests/Tests/Roslyn.Test/Tests/DisplayNameTestCollectionOrderer.cs @@ -0,0 +1,23 @@ +using Xunit; + +[assembly: TestCollectionOrderer("SpaceEngineers.Core.Roslyn.Test.Tests.DisplayNameTestCollectionOrderer", "SpaceEngineers.Core.Roslyn.Test")] +[assembly: CollectionBehavior(DisableTestParallelization = true)] + +namespace SpaceEngineers.Core.Roslyn.Test.Tests +{ + using System.Collections.Generic; + using System.Linq; + using Xunit.Abstractions; + + /// + /// DisplayNameTestCollectionOrderer + /// + public class DisplayNameTestCollectionOrderer : ITestCollectionOrderer + { + /// + public IEnumerable OrderTestCollections(IEnumerable testCollections) + { + return testCollections.OrderBy(collection => collection.DisplayName); + } + } +} \ No newline at end of file diff --git a/tests/Tests/Roslyn.Test/Tests/SolutionAnalysis.cs b/tests/Tests/Roslyn.Test/Tests/SolutionAnalysis.cs new file mode 100644 index 00000000..43322683 --- /dev/null +++ b/tests/Tests/Roslyn.Test/Tests/SolutionAnalysis.cs @@ -0,0 +1,85 @@ +namespace SpaceEngineers.Core.Roslyn.Test.Tests +{ + using System.Collections.Generic; + using System.Collections.Immutable; + using System.Threading; + using System.Threading.Tasks; + using Basics; + using Core.Test.Api.ClassFixtures; + using Extensions; + using Microsoft.CodeAnalysis.Diagnostics; + using Microsoft.CodeAnalysis.MSBuild; + using Xunit; + using Xunit.Abstractions; + + /// + /// Solution analysis test + /// + [Collection(nameof(SolutionAnalysis))] + public class SolutionAnalysis : AnalysisBase + { + private static readonly ImmutableArray IgnoredProjects + = ImmutableArray.Empty; + + private static readonly ImmutableArray IgnoredSources + = ImmutableArray.Create("AssemblyAttributes", + "Microsoft.NET.Test.Sdk.Program"); + + /// .cctor + /// ITestOutputHelper + /// TestFixture + public SolutionAnalysis(ITestOutputHelper output, TestFixture fixture) + : base(output, fixture) + { + } + + /// + protected override ImmutableArray IgnoredNamespaces => + ImmutableArray.Create("SpaceEngineers.Core.Roslyn.Test.Sources", + "SpaceEngineers.Core.Roslyn.Test.Sources.ComponentAttributeAnalyzer", + "SpaceEngineers.Core.Roslyn.Test.Sources.ComponentAttributeAnalyzerExpected"); + + [Fact] + internal async Task SolutionAnalysisTest() + { + var analyzers = DependencyContainer + .ResolveCollection() + .ToImmutableArray(); + + var diagnosticsCount = 0; + + var configuration = new Dictionary + { + { + "BuildingInsideVisualStudio", "false" + } + }; + using (var workspace = MSBuildWorkspace.Create(configuration)) + { + workspace.WorkspaceFailed += (_, workspaceFailedArgs) => + { + Output.WriteLine(workspaceFailedArgs.Diagnostic.Message); + }; + + await workspace.OpenSolutionAsync(SolutionExtensions.SolutionFile().FullName) + .ConfigureAwait(false); + + await foreach (var analyzedDocument in workspace + .CurrentSolution + .CompileSolution(analyzers, IgnoredProjects, IgnoredSources, IgnoredNamespaces) + .WithCancellation(CancellationToken.None) + .ConfigureAwait(false)) + { + foreach (var diagnostic in analyzedDocument.ActualDiagnostics) + { + Interlocked.Increment(ref diagnosticsCount); + Output.WriteLine($"[{diagnosticsCount}] " + diagnostic); + Output.WriteLine(diagnostic.Location.SourceTree.FilePath + ":" + (diagnostic.Location.GetLineSpan().Span.Start.Line + 1)); + } + } + } + + Assert.Equal(0, diagnosticsCount); + } + } +} \ No newline at end of file diff --git a/tests/Tests/Roslyn.Test/Tests/SourceMetadataReferenceProvider.cs b/tests/Tests/Roslyn.Test/Tests/SourceMetadataReferenceProvider.cs new file mode 100644 index 00000000..075068aa --- /dev/null +++ b/tests/Tests/Roslyn.Test/Tests/SourceMetadataReferenceProvider.cs @@ -0,0 +1,20 @@ +namespace SpaceEngineers.Core.Roslyn.Test.Tests +{ + using System.Collections.Generic; + using Abstractions; + using AutoRegistration.Api.Attributes; + using AutoRegistration.Api.Enumerations; + using Extensions; + using Microsoft.CodeAnalysis; + + /// + [Component(EnLifestyle.Singleton)] + internal class SourceMetadataReferenceProvider : IMetadataReferenceProvider + { + /// + public IEnumerable ReceiveReferences() + { + yield return typeof(Analysis).Assembly.AsMetadataReference(); + } + } +} \ No newline at end of file diff --git a/tests/Tests/Roslyn.Test/ValueObjects/AnalyzedDocument.cs b/tests/Tests/Roslyn.Test/ValueObjects/AnalyzedDocument.cs new file mode 100644 index 00000000..250feb7f --- /dev/null +++ b/tests/Tests/Roslyn.Test/ValueObjects/AnalyzedDocument.cs @@ -0,0 +1,37 @@ +namespace SpaceEngineers.Core.Roslyn.Test.ValueObjects +{ + using System.Collections.Immutable; + using Basics; + using Microsoft.CodeAnalysis; + + /// + /// Analyzed document + /// + public class AnalyzedDocument + { + /// .cctor + /// Document + /// Actual diagnostics + public AnalyzedDocument(Document document, ImmutableArray actualDiagnostics) + { + Name = document.Name.NameWithoutExtension(); + Document = document; + ActualDiagnostics = actualDiagnostics; + } + + /// + /// Name (without extension) + /// + public string Name { get; } + + /// + /// Document + /// + public Document Document { get; } + + /// + /// Actual diagnostics + /// + public ImmutableArray ActualDiagnostics { get; } + } +} \ No newline at end of file diff --git a/tests/Tests/Roslyn.Test/ValueObjects/DiagnosticLocation.cs b/tests/Tests/Roslyn.Test/ValueObjects/DiagnosticLocation.cs new file mode 100644 index 00000000..ad0de423 --- /dev/null +++ b/tests/Tests/Roslyn.Test/ValueObjects/DiagnosticLocation.cs @@ -0,0 +1,47 @@ +namespace SpaceEngineers.Core.Roslyn.Test.ValueObjects +{ + using System; + + /// + /// Location where the diagnostic appears, as determined by source file name, line number, and column number. + /// + public class DiagnosticLocation + { + /// .ctor + /// Name of source file (without extension) + /// Line + /// Column + /// Line/Column should be >= -1 + public DiagnosticLocation(string sourceFile, int line, int column) + { + if (line < -1) + { + throw new ArgumentOutOfRangeException(nameof(line), "line should be >= -1"); + } + + if (column < -1) + { + throw new ArgumentOutOfRangeException(nameof(column), "column should be >= -1"); + } + + SourceFile = sourceFile; + Line = line; + Column = column; + } + + /// + /// Name of source file (without extension) + /// + public string SourceFile { get; } + + /// + /// Line + /// + public int Line { get; } + + /// + /// Column + /// + public int Column { get; } + } +} \ No newline at end of file diff --git a/tests/Tests/Roslyn.Test/ValueObjects/ExpectedDiagnostic.cs b/tests/Tests/Roslyn.Test/ValueObjects/ExpectedDiagnostic.cs new file mode 100644 index 00000000..9fb19234 --- /dev/null +++ b/tests/Tests/Roslyn.Test/ValueObjects/ExpectedDiagnostic.cs @@ -0,0 +1,63 @@ +namespace SpaceEngineers.Core.Roslyn.Test.ValueObjects +{ + using System; + using Microsoft.CodeAnalysis; + + /// + /// Struct that stores information about a Diagnostic appearing in a source + /// + public class ExpectedDiagnostic + { + private DiagnosticLocation? _location; + + /// .cctor + /// DiagnosticDescriptor + /// Message + /// Diagnostic severity + public ExpectedDiagnostic(DiagnosticDescriptor descriptor, + string actualMessage, + DiagnosticSeverity severity) + { + Descriptor = descriptor; + ActualMessage = actualMessage; + Severity = severity; + } + + /// + /// Expected diagnostic location + /// + public DiagnosticLocation Location => _location ?? throw new InvalidOperationException($"Use {nameof(WithLocation)} before equality comparision"); + + /// + /// Diagnostic descriptor + /// + public DiagnosticDescriptor Descriptor { get; } + + /// + /// Actual message + /// + public string ActualMessage { get; } + + /// + /// Diagnostic severity + /// + public DiagnosticSeverity Severity { get; } + + /// + /// WithLocation + /// + /// Source file name (with extension) + /// line + /// column + /// Expected diagnostic with specified location + public ExpectedDiagnostic WithLocation(string sourceFileName, int line, int column) + { + var copy = new ExpectedDiagnostic(Descriptor, ActualMessage, DiagnosticSeverity.Error) + { + _location = new DiagnosticLocation(sourceFileName, line, column) + }; + + return copy; + } + } +} diff --git a/tests/Tests/Roslyn.Test/ValueObjects/SourceFile.cs b/tests/Tests/Roslyn.Test/ValueObjects/SourceFile.cs new file mode 100644 index 00000000..b1283e7d --- /dev/null +++ b/tests/Tests/Roslyn.Test/ValueObjects/SourceFile.cs @@ -0,0 +1,32 @@ +namespace SpaceEngineers.Core.Roslyn.Test.ValueObjects +{ + using Microsoft.CodeAnalysis.Text; + + /// + /// Source file value object + /// + public class SourceFile + { + /// .cctor + /// Source file name (without extension) + /// Source file text + public SourceFile(string name, SourceText text) + { + Name = name; + Text = text; + } + + /// + /// Source file name (without extension) + /// + public string Name { get; } + + /// + /// Source file text + /// + public SourceText Text { get; } + + /// + public override string ToString() => Name; + } +} \ No newline at end of file diff --git a/tests/Tests/Test.Api/Abstractions/IModulesTestFixture.cs b/tests/Tests/Test.Api/Abstractions/IModulesTestFixture.cs new file mode 100644 index 00000000..470c8440 --- /dev/null +++ b/tests/Tests/Test.Api/Abstractions/IModulesTestFixture.cs @@ -0,0 +1,40 @@ +namespace SpaceEngineers.Core.Test.Api.Abstractions +{ + using System; + using CompositionRoot; + using CompositionRoot.Registration; + using Microsoft.Extensions.Hosting; + + /// + /// IModulesTestFixture + /// + public interface IModulesTestFixture + { + /// + /// Creates and configures IHostBuilder + /// + /// IHostBuilder + IHostBuilder CreateHostBuilder(); + + /// + /// Generates IManualRegistration object with specified delegate + /// + /// Registration action + /// IManualRegistration + IManualRegistration DelegateRegistration(Action registrationAction); + + /// + /// Generates IComponentsOverride object with specified delegate + /// + /// Override action + /// IComponentsOverride + IComponentsOverride DelegateOverride(Action overrideAction); + + /// + /// Creates IDependencyContainer + /// + /// Dependency container options + /// IDependencyContainer + public IDependencyContainer DependencyContainer(DependencyContainerOptions options); + } +} \ No newline at end of file diff --git a/tests/Tests/Test.Api/ClassFixtures/TestFixture.cs b/tests/Tests/Test.Api/ClassFixtures/TestFixture.cs new file mode 100644 index 00000000..7301442c --- /dev/null +++ b/tests/Tests/Test.Api/ClassFixtures/TestFixture.cs @@ -0,0 +1,77 @@ +namespace SpaceEngineers.Core.Test.Api.ClassFixtures +{ + using System; + using System.Collections.Concurrent; + using System.Linq; + using System.Reflection; + using Abstractions; + using CompositionRoot; + using CompositionRoot.Registration; + using Microsoft.Extensions.Hosting; + using Microsoft.Extensions.Logging; + using Microsoft.Extensions.Logging.Console; + using Registrations; + + /// + /// TestFixture + /// + public sealed class TestFixture : IModulesTestFixture + { + private static readonly ConcurrentDictionary Cache = new ConcurrentDictionary(); + + /// + public IHostBuilder CreateHostBuilder() + { + return Host + .CreateDefaultBuilder() + .ConfigureLogging((context, builder) => + { + builder.ClearProviders(); + builder.AddConfiguration(context.Configuration.GetSection("Logging")); + builder + .AddSimpleConsole(options => + { + options.ColorBehavior = LoggerColorBehavior.Disabled; + options.SingleLine = false; + options.IncludeScopes = false; + options.TimestampFormat = null; + }) + .SetMinimumLevel(LogLevel.Trace); + }); + } + + /// + public IManualRegistration DelegateRegistration(Action registrationAction) + { + return new ManualDelegateRegistration(registrationAction); + } + + /// + public IComponentsOverride DelegateOverride(Action overrideAction) + { + return new DelegateComponentsOverride(overrideAction); + } + + /// + public IDependencyContainer DependencyContainer(DependencyContainerOptions options) + { + var hash = DependencyContainerHash(options); + + if (Cache.TryGetValue(hash, out var container)) + { + return container; + } + + container = CompositionRoot.DependencyContainer.Create(options); + + return Cache.AddOrUpdate(hash, _ => container, (_, _) => container); + } + + private static int DependencyContainerHash( + DependencyContainerOptions options, + params Assembly[] aboveAssembly) + { + return HashCode.Combine(aboveAssembly.Aggregate(int.MaxValue, HashCode.Combine), options); + } + } +} \ No newline at end of file diff --git a/tests/Tests/Test.Api/Logging/DebugListener.cs b/tests/Tests/Test.Api/Logging/DebugListener.cs new file mode 100644 index 00000000..af34cb4e --- /dev/null +++ b/tests/Tests/Test.Api/Logging/DebugListener.cs @@ -0,0 +1,16 @@ +namespace SpaceEngineers.Core.Test.Api.Logging +{ + using System; + using Basics; + + internal static class DebugListener + { + internal static Action Write { get; } = value => + { + if (!value.IsNullOrEmpty()) + { + TestBase.Local.Value?.Output.WriteLine(value.TrimEnd('\r', '\n')); + } + }; + } +} \ No newline at end of file diff --git a/tests/Tests/Test.Api/Logging/TestOutputTextWriter.cs b/tests/Tests/Test.Api/Logging/TestOutputTextWriter.cs new file mode 100644 index 00000000..feb948d6 --- /dev/null +++ b/tests/Tests/Test.Api/Logging/TestOutputTextWriter.cs @@ -0,0 +1,55 @@ +namespace SpaceEngineers.Core.Test.Api.Logging +{ + using System.IO; + using System.Text; + using System.Threading.Tasks; + + internal class TestOutputTextWriter : TextWriter + { + public override Encoding Encoding { get; } = Encoding.UTF8; + + public override void Write(char value) + { + TestBase.Local.Value?.Output.WriteLine(value.ToString()); + } + + public override void Write(string? value) + { + TestBase.Local.Value?.Output.WriteLine(value?.TrimEnd('\r', '\n') ?? string.Empty); + } + + public override void WriteLine() + { + TestBase.Local.Value?.Output.WriteLine(string.Empty); + } + + public override void WriteLine(string? value) + { + TestBase.Local.Value?.Output.WriteLine(value?.TrimEnd('\r', '\n') ?? string.Empty); + } + + public override Task WriteAsync(char value) + { + Write(value); + return Task.CompletedTask; + } + + public override Task WriteAsync(string? value) + { + Write(value); + return Task.CompletedTask; + } + + public override Task WriteLineAsync() + { + WriteLine(); + return Task.CompletedTask; + } + + public override Task WriteLineAsync(string? value) + { + WriteLine(value); + return Task.CompletedTask; + } + } +} \ No newline at end of file diff --git a/tests/Tests/Test.Api/Logging/TraceListener.cs b/tests/Tests/Test.Api/Logging/TraceListener.cs new file mode 100644 index 00000000..6f607ccb --- /dev/null +++ b/tests/Tests/Test.Api/Logging/TraceListener.cs @@ -0,0 +1,22 @@ +namespace SpaceEngineers.Core.Test.Api.Logging +{ + using Basics; + + internal class TraceListener : System.Diagnostics.TraceListener + { + public override bool IsThreadSafe => true; + + public override void Write(string? value) + { + if (!value.IsNullOrEmpty()) + { + TestBase.Local.Value?.Output.WriteLine(value.TrimEnd('\r', '\n')); + } + } + + public override void WriteLine(string? value) + { + TestBase.Local.Value?.Output.WriteLine(value?.TrimEnd('\r', '\n') ?? string.Empty); + } + } +} \ No newline at end of file diff --git a/tests/Tests/Test.Api/Logging/XUnitModuleInitializer.cs b/tests/Tests/Test.Api/Logging/XUnitModuleInitializer.cs new file mode 100644 index 00000000..4f6f5541 --- /dev/null +++ b/tests/Tests/Test.Api/Logging/XUnitModuleInitializer.cs @@ -0,0 +1,12 @@ +namespace SpaceEngineers.Core.Test.Api.Logging +{ + using System.Diagnostics.CodeAnalysis; + using System.Runtime.CompilerServices; + + internal static class XUnitModuleInitializer + { + [ModuleInitializer] + [SuppressMessage("Analysis", "CA2255", Justification = "Redirects outputs to xUnit ITestOutputHelper")] + public static void Initialize() => TestBase.Redirect(); + } +} \ No newline at end of file diff --git a/tests/Tests/Test.Api/Properties/AssemblyInfo.cs b/tests/Tests/Test.Api/Properties/AssemblyInfo.cs new file mode 100644 index 00000000..31adb06d --- /dev/null +++ b/tests/Tests/Test.Api/Properties/AssemblyInfo.cs @@ -0,0 +1,5 @@ +using System.Reflection; + +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] +[assembly: AssemblyInformationalVersion("1.0.0.0")] \ No newline at end of file diff --git a/tests/Tests/Test.Api/Registrations/DelegateComponentsOverride.cs b/tests/Tests/Test.Api/Registrations/DelegateComponentsOverride.cs new file mode 100644 index 00000000..a19c9528 --- /dev/null +++ b/tests/Tests/Test.Api/Registrations/DelegateComponentsOverride.cs @@ -0,0 +1,25 @@ +namespace SpaceEngineers.Core.Test.Api.Registrations +{ + using System; + using CompositionRoot.Registration; + + internal class DelegateComponentsOverride : IComponentsOverride + { + private readonly Action _overrideAction; + + public DelegateComponentsOverride(Action overrideAction) + { + _overrideAction = overrideAction; + } + + public void RegisterOverrides(IRegisterComponentsOverrideContainer container) + { + _overrideAction(container); + } + + public override int GetHashCode() + { + return _overrideAction.GetHashCode(); + } + } +} \ No newline at end of file diff --git a/tests/Tests/Test.Api/Registrations/ManualDelegateRegistration.cs b/tests/Tests/Test.Api/Registrations/ManualDelegateRegistration.cs new file mode 100644 index 00000000..d1ee4a09 --- /dev/null +++ b/tests/Tests/Test.Api/Registrations/ManualDelegateRegistration.cs @@ -0,0 +1,25 @@ +namespace SpaceEngineers.Core.Test.Api.Registrations +{ + using System; + using CompositionRoot.Registration; + + internal class ManualDelegateRegistration : IManualRegistration + { + private readonly Action _registrationAction; + + public ManualDelegateRegistration(Action registrationAction) + { + _registrationAction = registrationAction; + } + + public void Register(IManualRegistrationsContainer container) + { + _registrationAction(container); + } + + public override int GetHashCode() + { + return _registrationAction.GetHashCode(); + } + } +} \ No newline at end of file diff --git a/tests/Tests/Test.Api/SystemTypeTestExtensions.cs b/tests/Tests/Test.Api/SystemTypeTestExtensions.cs new file mode 100644 index 00000000..59fdf6bb --- /dev/null +++ b/tests/Tests/Test.Api/SystemTypeTestExtensions.cs @@ -0,0 +1,30 @@ +namespace SpaceEngineers.Core.Test.Api +{ + using System; + using System.Collections.Generic; + using System.Linq; + + /// + /// System.Type extensions for test project + /// + public static class SystemTypeTestExtensions + { + /// Show system types + /// Source + /// Tag + /// Show action + /// Types projection + public static IEnumerable ShowTypes( + this IEnumerable source, + string tag, + Action show) + { + show(tag); + return source.Select(type => + { + show(type.FullName ?? "null"); + return type; + }); + } + } +} \ No newline at end of file diff --git a/tests/Tests/Test.Api/Test.Api.csproj b/tests/Tests/Test.Api/Test.Api.csproj new file mode 100644 index 00000000..199b8c77 --- /dev/null +++ b/tests/Tests/Test.Api/Test.Api.csproj @@ -0,0 +1,25 @@ + + + + net7.0 + SpaceEngineers.Core.Test.Api + SpaceEngineers.Core.Test.Api + true + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + diff --git a/tests/Tests/Test.Api/TestBase.cs b/tests/Tests/Test.Api/TestBase.cs new file mode 100644 index 00000000..efa93af5 --- /dev/null +++ b/tests/Tests/Test.Api/TestBase.cs @@ -0,0 +1,74 @@ +namespace SpaceEngineers.Core.Test.Api +{ + using System; + using System.Diagnostics; + using System.Diagnostics.CodeAnalysis; + using System.Reflection; + using System.Threading; + using Basics; + using ClassFixtures; + using Logging; + using Xunit; + using Xunit.Abstractions; + using Xunit.Sdk; + using TraceListener = Logging.TraceListener; + + /// + /// TestBase + /// + public abstract class TestBase : IClassFixture, + IDisposable + { + internal static readonly AsyncLocal Local = new AsyncLocal(); + + /// .cctor + /// ITestOutputHelper + /// TestFixture + protected TestBase(ITestOutputHelper output, TestFixture fixture) + { + Output = output; + Fixture = fixture; + + Local.Value ??= this; + } + + /// + /// ITestOutputHelper + /// + public ITestOutputHelper Output { get; } + + /// + /// TestFixture + /// + public TestFixture Fixture { get; } + + /// + /// TestCase + /// + public IXunitTestCase TestCase => (IXunitTestCase)Output.GetFieldValue("test").TestCase; + + /// + /// Redirects outputs to xUnit ITestOutputHelper + /// + [SuppressMessage("Analysis", "CA2000", Justification = "IDbConnection will be disposed in outer scope by client")] + public static void Redirect() + { + Trace.Listeners.Clear(); + Trace.Listeners.Add(new TraceListener()); + + Type.GetType("System.Diagnostics.DebugProvider") + .GetField("s_WriteCore", BindingFlags.Static | BindingFlags.NonPublic) + .SetValue(null, DebugListener.Write); + + var writer = new TestOutputTextWriter(); + Console.SetOut(writer); + Console.SetError(writer); + } + + /// + public void Dispose() + { + Local.Value = null; + } + } +} \ No newline at end of file diff --git a/tests/Tests/Test.WebApplication/ComposeSettings/appsettings.json b/tests/Tests/Test.WebApplication/ComposeSettings/appsettings.json new file mode 100644 index 00000000..31f1a152 --- /dev/null +++ b/tests/Tests/Test.WebApplication/ComposeSettings/appsettings.json @@ -0,0 +1,61 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "System": "Information", + "Microsoft": "Information", + "Npgsql": "Information" + } + }, + "AllowedHosts": "*", + "Endpoints":{ + "TransportEndpointGateway": { + "GenericEndpointSettings": { + "RpcRequestSecondsTimeout": 60 + }, + "RabbitMqSettings": { + "Hosts": [ + "rabbit" + ], + "Port": "5672", + "HttpApiPort": "15672", + "User": "guest", + "Password": "guest", + "VirtualHost": "WebApplication", + "ApplicationName": "WebApplication", + "ConsumerPrefetchCount": 100, + "QueueMaxLengthBytes": 1048576, + "ConsumerPriority": 0 + } + }, + "AuthEndpoint": { + "Authorization": { + "Issuer": "Test", + "Audience": "Test", + "PrivateKey": "db3OIsj+BXE9NZDy0t8W3TcNekrF+2d/1sFnWG4HnV8TZY30iTOdtVWJG8abWvB1GlOgJuQZdcF2Luqm/hccMw==" + }, + "AuthorizationSettings": { + "TokenExpirationMinutesTimeout": 5 + }, + "GenericEndpointSettings": { + "RpcRequestSecondsTimeout": 60 + }, + "OrmSettings": { + "CommandSecondsTimeout": 60 + }, + "OutboxSettings": { + "OutboxDeliverySecondsInterval": 60 + }, + "SqlDatabaseSettings": { + "ApplicationName": "WebApplication", + "Host": "postgres", + "Port": 5432, + "Database": "WebApplication", + "IsolationLevel": "ReadCommitted", + "Username": "postgres", + "Password": "Password12!", + "ConnectionPoolSize": 1 + } + } + } +} \ No newline at end of file diff --git a/tests/Tests/Test.WebApplication/Features.cs b/tests/Tests/Test.WebApplication/Features.cs new file mode 100644 index 00000000..58b3509b --- /dev/null +++ b/tests/Tests/Test.WebApplication/Features.cs @@ -0,0 +1,7 @@ +namespace SpaceEngineers.Core.Test.WebApplication +{ + internal static class Features + { + public const string WebApiTest = nameof(WebApiTest); + } +} \ No newline at end of file diff --git a/tests/Tests/Test.WebApplication/Identity.cs b/tests/Tests/Test.WebApplication/Identity.cs new file mode 100644 index 00000000..4c2b09e9 --- /dev/null +++ b/tests/Tests/Test.WebApplication/Identity.cs @@ -0,0 +1,34 @@ +namespace SpaceEngineers.Core.Test.WebApplication +{ + using System.Diagnostics.CodeAnalysis; + using System.Reflection; + using Basics; + using GenericEndpoint.Contract; + + /// + /// Identity + /// + [SuppressMessage("Analysis", "CA1724", Justification = "desired name")] + public static class Identity + { + /// + /// TestEndpoint logical name + /// + public const string LogicalName = "TestEndpoint"; + + /// + /// TestEndpoint assembly + /// + public static Assembly Assembly { get; } = AssembliesExtensions.FindRequiredAssembly( + AssembliesExtensions.BuildName( + nameof(SpaceEngineers), + nameof(Core), + nameof(Test), + nameof(WebApplication))); + + /// + /// TestEndpoint identity + /// + public static EndpointIdentity EndpointIdentity { get; } = new EndpointIdentity(LogicalName, Assembly); + } +} \ No newline at end of file diff --git a/tests/Tests/Test.WebApplication/Migrations/AddSeedDataMigration.cs b/tests/Tests/Test.WebApplication/Migrations/AddSeedDataMigration.cs new file mode 100644 index 00000000..2f63fbce --- /dev/null +++ b/tests/Tests/Test.WebApplication/Migrations/AddSeedDataMigration.cs @@ -0,0 +1,71 @@ +namespace SpaceEngineers.Core.Test.WebApplication.Migrations +{ + using System; + using System.Linq; + using System.Threading; + using System.Threading.Tasks; + using AuthEndpoint.Domain; + using AutoRegistration.Api.Abstractions; + using AutoRegistration.Api.Attributes; + using AutoRegistration.Api.Enumerations; + using Basics.Attributes; + using CompositionRoot; + using DataAccess.Orm.Sql.Linq; + using DataAccess.Orm.Sql.Migrations.Internals; + using DataAccess.Orm.Sql.Model; + using DataAccess.Orm.Sql.Transaction; + using GenericDomain.Api.Abstractions; + using GenericDomain.EventSourcing; + + [Component(EnLifestyle.Singleton)] + [After("SpaceEngineers.Core.DataAccess.Orm.Sql.Migrations SpaceEngineers.Core.DataAccess.Orm.Sql.Migrations.Internals.ApplyDeltaMigration")] + internal class AddSeedDataMigration : BaseAddSeedDataMigration, + ICollectionResolvable + { + private readonly IDependencyContainer _dependencyContainer; + + public AddSeedDataMigration(IDependencyContainer dependencyContainer) + { + _dependencyContainer = dependencyContainer; + } + + public sealed override string Name { get; } = nameof(AddSeedDataMigration); + + protected sealed override async Task AddSeedData( + IAdvancedDatabaseTransaction transaction, + CancellationToken token) + { + var username = "qwerty"; + var password = "12345678"; + + var aggregateId = Guid.NewGuid(); + + var salt = Password.GenerateSalt(); + var passwordHash = new Password(password).GeneratePasswordHash(salt); + + var domainEvents = new IDomainEvent[] + { + new UserWasCreated(aggregateId, new Username(username), salt, passwordHash), + new PermissionWasGranted(new Feature(AuthEndpoint.Contract.Features.Authentication)), + new PermissionWasGranted(new Feature(Features.WebApiTest)) + }; + + var args = domainEvents + .Select((domainEvent, index) => new DomainEventArgs(aggregateId, domainEvent, index, DateTime.UtcNow)) + .ToArray(); + + await _dependencyContainer + .Resolve() + .Append(args, token) + .ConfigureAwait(false); + + var userDatabaseEntity = new AuthEndpoint.DatabaseModel.User(aggregateId, username); + + await transaction + .Insert(new IDatabaseEntity[] { userDatabaseEntity }, EnInsertBehavior.Default) + .CachedExpression("4271E906-F346-46FB-877C-675818B148E5") + .Invoke(token) + .ConfigureAwait(false); + } + } +} \ No newline at end of file diff --git a/tests/Tests/Test.WebApplication/Program.cs b/tests/Tests/Test.WebApplication/Program.cs new file mode 100644 index 00000000..aa75a917 --- /dev/null +++ b/tests/Tests/Test.WebApplication/Program.cs @@ -0,0 +1,137 @@ +namespace SpaceEngineers.Core.Test.WebApplication +{ + using System; + using System.Diagnostics.CodeAnalysis; + using System.IO; + using System.Text.Json; + using System.Threading.Tasks; + using AuthEndpoint.Host; + using Basics; + using GenericEndpoint.Authorization.Host; + using GenericEndpoint.Authorization.Web.Host; + using GenericEndpoint.DataAccess.Sql.Postgres.Host; + using GenericEndpoint.EventSourcing.Host; + using GenericEndpoint.Host; + using GenericEndpoint.Telemetry.Host; + using GenericEndpoint.Web.Host; + using GenericHost; + using IntegrationTransport.Host; + using Microsoft.Extensions.Hosting; + using Microsoft.Extensions.Logging; + using Migrations; + using Registrations; + using StartupActions; + + /// + /// Program + /// + public static class Program + { + /// Main + /// args + /// Ongoing operation + [SuppressMessage("Analysis", "CA1506", Justification = "web application composition root")] + public static Task Main(string[] args) + { + var projectFileDirectory = SolutionExtensions.ProjectFile().Directory + ?? throw new InvalidOperationException("Project directory wasn't found"); + + var settingsDirectory = projectFileDirectory.StepInto("Settings"); + + return BuildHost(settingsDirectory, args).RunAsync(); + } + + /// + /// BuildHost + /// + /// Settings directory + /// args + /// IHost + [SuppressMessage("Analysis", "CA1506", Justification = "application composition root")] + public static IHost BuildHost(DirectoryInfo settingsDirectory, string[] args) + { + return Host + .CreateDefaultBuilder(args) + + /* + * logging + */ + + .ConfigureLogging((context, builder) => + { + builder.ClearProviders(); + builder.AddConfiguration(context.Configuration.GetSection("Logging")); + builder.AddJsonConsole(options => + { + options.JsonWriterOptions = new JsonWriterOptions + { + Indented = true + }; + options.IncludeScopes = false; + }) + .SetMinimumLevel(LogLevel.Trace); + }) + + /* + * IntegrationTransport + */ + + .UseRabbitMqIntegrationTransport( + IntegrationTransport.RabbitMQ.Identity.TransportIdentity(), + options => options.WithManualRegistrations(new PurgeRabbitMqQueuesManualRegistration())) + + /* + * AuthEndpoint + */ + + .UseAuthEndpoint(builder => builder + .WithPostgreSqlDataAccess(options => options.ExecuteMigrations()) + .WithSqlEventSourcing() + .WithOpenTelemetry() + .WithJwtAuthentication(builder.Context.Configuration) + .WithAuthorization() + .WithWebAuthorization() + .WithWebApi() + .ModifyContainerOptions(options => options + .WithAdditionalOurTypes( + typeof(RecreatePostgreSqlDatabaseHostedServiceStartupAction), + typeof(AddSeedDataMigration))) + .BuildOptions()) + + /* + * TestEndpoint + */ + + .UseEndpoint( + Identity.EndpointIdentity, + builder => builder + .WithOpenTelemetry() + .WithJwtAuthentication(builder.Context.Configuration) + .WithAuthorization() + .WithWebAuthorization() + .WithWebApi() + .ModifyContainerOptions(options => options + .WithAdditionalOurTypes(typeof(TestController))) + .BuildOptions()) + + /* + * WebApiGateway + */ + + .UseWebApiGateway() + + /* + * Telemetry + */ + .UseOpenTelemetry() + + /* + * Building + */ + + .UseEnvironment(Environments.Development) + + .BuildHost(settingsDirectory); + } + } +} \ No newline at end of file diff --git a/tests/Tests/Test.WebApplication/Properties/AssemblyInfo.cs b/tests/Tests/Test.WebApplication/Properties/AssemblyInfo.cs new file mode 100644 index 00000000..01097201 --- /dev/null +++ b/tests/Tests/Test.WebApplication/Properties/AssemblyInfo.cs @@ -0,0 +1,11 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using Microsoft.AspNetCore.Mvc; + +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] +[assembly: AssemblyInformationalVersion("1.0.0.0")] + +[assembly: ApiController] + +[assembly:InternalsVisibleTo("SpaceEngineers.Core.WebApplication.Test")] \ No newline at end of file diff --git a/tests/Tests/Test.WebApplication/Properties/launchSettings.json b/tests/Tests/Test.WebApplication/Properties/launchSettings.json new file mode 100644 index 00000000..c7b73cd0 --- /dev/null +++ b/tests/Tests/Test.WebApplication/Properties/launchSettings.json @@ -0,0 +1,13 @@ +{ + "profiles": { + "Client": { + "commandName": "Project", + "launchBrowser": false, + "launchUrl": "api/swagger", + "applicationUrl": "http://localhost:5000", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/tests/Tests/Test.WebApplication/Registrations/PurgeRabbitMqQueuesManualRegistration.cs b/tests/Tests/Test.WebApplication/Registrations/PurgeRabbitMqQueuesManualRegistration.cs new file mode 100644 index 00000000..e80ca3a4 --- /dev/null +++ b/tests/Tests/Test.WebApplication/Registrations/PurgeRabbitMqQueuesManualRegistration.cs @@ -0,0 +1,17 @@ +namespace SpaceEngineers.Core.Test.WebApplication.Registrations +{ + using AutoRegistration.Api.Enumerations; + using CompositionRoot.Registration; + using GenericHost; + using StartupActions; + + internal class PurgeRabbitMqQueuesManualRegistration : IManualRegistration + { + public void Register(IManualRegistrationsContainer container) + { + container.Register(EnLifestyle.Singleton); + container.Advanced.RegisterCollectionEntry(EnLifestyle.Singleton); + container.Advanced.RegisterCollectionEntry(EnLifestyle.Singleton); + } + } +} \ No newline at end of file diff --git a/tests/Tests/Test.WebApplication/Settings/appsettings.json b/tests/Tests/Test.WebApplication/Settings/appsettings.json new file mode 100644 index 00000000..f398d3ee --- /dev/null +++ b/tests/Tests/Test.WebApplication/Settings/appsettings.json @@ -0,0 +1,60 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "System": "Information", + "Microsoft": "Information", + "Npgsql": "Information" + } + }, + "AllowedHosts": "*", + "Transports": { + "RabbitMqIntegrationTransport": { + "RabbitMqSettings": { + "Hosts": [ + "localhost" + ], + "Port": "5672", + "HttpApiPort": "15672", + "User": "guest", + "Password": "guest", + "VirtualHost": "WebApplication", + "ApplicationName": "WebApplication", + "ConsumerPrefetchCount": 100, + "QueueMaxLengthBytes": 1048576, + "ConsumerPriority": 0 + } + } + }, + "Endpoints": { + "AuthEndpoint": { + "Authorization": { + "Issuer": "Test", + "Audience": "Test", + "PrivateKey": "db3OIsj+BXE9NZDy0t8W3TcNekrF+2d/1sFnWG4HnV8TZY30iTOdtVWJG8abWvB1GlOgJuQZdcF2Luqm/hccMw==" + }, + "AuthorizationSettings": { + "TokenExpirationMinutesTimeout": 5 + }, + "GenericEndpointSettings": { + "RpcRequestSecondsTimeout": 60 + }, + "OrmSettings": { + "CommandSecondsTimeout": 60 + }, + "OutboxSettings": { + "OutboxDeliverySecondsInterval": 60 + }, + "SqlDatabaseSettings": { + "ApplicationName": "WebApplication", + "Host": "localhost", + "Port": 5432, + "Database": "WebApplication", + "IsolationLevel": "ReadCommitted", + "Username": "postgres", + "Password": "Password12!", + "ConnectionPoolSize": 1 + } + } + } +} \ No newline at end of file diff --git a/tests/Tests/Test.WebApplication/StartupActions/PurgeRabbitMqQueuesHostedServiceStartupAction.cs b/tests/Tests/Test.WebApplication/StartupActions/PurgeRabbitMqQueuesHostedServiceStartupAction.cs new file mode 100644 index 00000000..da494b6c --- /dev/null +++ b/tests/Tests/Test.WebApplication/StartupActions/PurgeRabbitMqQueuesHostedServiceStartupAction.cs @@ -0,0 +1,44 @@ +namespace SpaceEngineers.Core.Test.WebApplication.StartupActions +{ + using System.Threading; + using System.Threading.Tasks; + using AutoRegistration.Api.Abstractions; + using AutoRegistration.Api.Attributes; + using Basics.Attributes; + using CrossCuttingConcerns.Json; + using CrossCuttingConcerns.Settings; + using GenericHost; + using IntegrationTransport.RabbitMQ.Extensions; + using Microsoft.Extensions.Logging; + using SpaceEngineers.Core.IntegrationTransport.RabbitMQ.Settings; + + [ManuallyRegisteredComponent("Hosting dependency that implicitly participates in composition")] + [Before("SpaceEngineers.Core.GenericEndpoint.Host SpaceEngineers.Core.GenericEndpoint.Host.StartupActions.GenericEndpointHostedServiceStartupAction")] + internal class PurgeRabbitMqQueuesHostedServiceStartupAction : IHostedServiceStartupAction, + ICollectionResolvable, + ICollectionResolvable, + IResolvable + { + private readonly RabbitMqSettings _rabbitMqSettings; + private readonly IJsonSerializer _jsonSerializer; + private readonly ILogger _logger; + + public PurgeRabbitMqQueuesHostedServiceStartupAction( + ISettingsProvider rabbitMqSettingsProvider, + IJsonSerializer jsonSerializer, + ILogger logger) + { + _rabbitMqSettings = rabbitMqSettingsProvider.Get(); + + _jsonSerializer = jsonSerializer; + _logger = logger; + } + + public async Task Run(CancellationToken token) + { + await _rabbitMqSettings + .PurgeMessages(_jsonSerializer, _logger, token) + .ConfigureAwait(false); + } + } +} \ No newline at end of file diff --git a/tests/Tests/Test.WebApplication/StartupActions/RecreatePostgreSqlDatabaseHostedServiceStartupAction.cs b/tests/Tests/Test.WebApplication/StartupActions/RecreatePostgreSqlDatabaseHostedServiceStartupAction.cs new file mode 100644 index 00000000..df7b0fc4 --- /dev/null +++ b/tests/Tests/Test.WebApplication/StartupActions/RecreatePostgreSqlDatabaseHostedServiceStartupAction.cs @@ -0,0 +1,99 @@ +namespace SpaceEngineers.Core.Test.WebApplication.StartupActions +{ + using System; + using System.Diagnostics.CodeAnalysis; + using System.Threading; + using System.Threading.Tasks; + using AutoRegistration.Api.Abstractions; + using AutoRegistration.Api.Attributes; + using AutoRegistration.Api.Enumerations; + using Basics; + using CompositionRoot; + using CrossCuttingConcerns.Settings; + using DataAccess.Orm.Sql.Connection; + using DataAccess.Orm.Sql.Settings; + using DataAccess.Orm.Sql.Translation; + using GenericHost; + using Npgsql; + + [Component(EnLifestyle.Singleton)] + internal class RecreatePostgreSqlDatabaseHostedServiceStartupAction : IHostedServiceStartupAction, + ICollectionResolvable, + ICollectionResolvable, + IResolvable + { + private const string CommandText = @"create extension if not exists dblink; + +drop database if exists ""{0}"" with (FORCE); +create database ""{0}""; +grant all privileges on database ""{0}"" to ""{1}"";"; + + private readonly SqlDatabaseSettings _sqlDatabaseSettings; + private readonly IDependencyContainer _dependencyContainer; + private readonly IDatabaseConnectionProvider _connectionProvider; + + public RecreatePostgreSqlDatabaseHostedServiceStartupAction( + IDependencyContainer dependencyContainer, + ISettingsProvider sqlDatabaseSettingsProvider, + IDatabaseConnectionProvider connectionProvider) + { + _sqlDatabaseSettings = sqlDatabaseSettingsProvider.Get(); + + _dependencyContainer = dependencyContainer; + _connectionProvider = connectionProvider; + } + + [SuppressMessage("Analysis", "CA2000", Justification = "IDbConnection will be disposed in outer scope by client")] + public async Task Run(CancellationToken token) + { + var command = new SqlCommand( + CommandText.Format(_sqlDatabaseSettings.Database, _sqlDatabaseSettings.Username), + Array.Empty()); + + var connectionStringBuilder = new NpgsqlConnectionStringBuilder + { + Host = _sqlDatabaseSettings.Host, + Port = _sqlDatabaseSettings.Port, + Database = "postgres", + Username = _sqlDatabaseSettings.Username, + Password = _sqlDatabaseSettings.Password + }; + + var npgSqlConnection = new NpgsqlConnection(connectionStringBuilder.ConnectionString); + + try + { + await npgSqlConnection.OpenAsync(token).ConfigureAwait(false); + + _ = await _connectionProvider + .Execute(npgSqlConnection, command, token) + .ConfigureAwait(false); + } + finally + { + npgSqlConnection.Dispose(); + } + + NpgsqlConnection.ClearPool(npgSqlConnection); + + while (true) + { + var doesDatabaseExist = await _dependencyContainer + .Resolve() + .DoesDatabaseExist(token) + .ConfigureAwait(false); + + if (!doesDatabaseExist) + { + await Task + .Delay(TimeSpan.FromMilliseconds(100), token) + .ConfigureAwait(false); + } + else + { + break; + } + } + } + } +} \ No newline at end of file diff --git a/tests/Tests/Test.WebApplication/Test.WebApplication.csproj b/tests/Tests/Test.WebApplication/Test.WebApplication.csproj new file mode 100644 index 00000000..d950d893 --- /dev/null +++ b/tests/Tests/Test.WebApplication/Test.WebApplication.csproj @@ -0,0 +1,30 @@ + + + net7.0 + SpaceEngineers.Core.Test.WebApplication + SpaceEngineers.Core.Test.WebApplication + false + true + true + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + diff --git a/tests/Tests/Test.WebApplication/TestController.cs b/tests/Tests/Test.WebApplication/TestController.cs new file mode 100644 index 00000000..849f741f --- /dev/null +++ b/tests/Tests/Test.WebApplication/TestController.cs @@ -0,0 +1,156 @@ +namespace SpaceEngineers.Core.Test.WebApplication +{ + using System; + using System.Diagnostics.CodeAnalysis; + using System.Linq; + using System.Net; + using System.Net.Mime; + using System.Security.Claims; + using System.Threading; + using System.Threading.Tasks; + using CrossCuttingConcerns.Json; + using GenericEndpoint.Contract; + using GenericEndpoint.Contract.Attributes; + using Microsoft.AspNetCore.Mvc; + using Microsoft.AspNetCore.Routing; + using RestSharp; + using Web.Api; + using Web.Api.Containers; + + /// + /// Test controller + /// + [Route("api/[controller]/[action]")] + [EndpointGroupName(Identity.LogicalName)] + [ApiController] + [Feature(Features.WebApiTest)] + public class TestController : ControllerBase + { + private readonly EndpointIdentity _endpointIdentity; + private readonly IJsonSerializer _jsonSerializer; + private readonly IDataContainersProvider _dataContainersProvider; + + /// .cctor + /// EndpointIdentity + /// IJsonSerializer + /// IDataContainersProvider + public TestController( + EndpointIdentity endpointIdentity, + IJsonSerializer jsonSerializer, + IDataContainersProvider dataContainersProvider) + { + _endpointIdentity = endpointIdentity; + _jsonSerializer = jsonSerializer; + _dataContainersProvider = dataContainersProvider; + } + + /// + /// Anonymously gets application info + /// + /// Application info + [HttpGet] + [Microsoft.AspNetCore.Authorization.AllowAnonymous] + [SuppressMessage("Analysis", "CA1031", Justification = "desired behavior")] + public async Task>> ApplicationInfo() + { + var response = new ScalarResponse(); + + try + { + response.WithItem($"{_endpointIdentity}"); + } + catch (Exception exception) + { + response.WithError(exception); + } + + var result = new JsonResult(response) + { + ContentType = MediaTypeNames.Application.Json, + StatusCode = (int)(response.Success ? HttpStatusCode.OK : HttpStatusCode.InternalServerError) + }; + + return await Task.FromResult(result).ConfigureAwait(false); + } + + /// + /// Gets authorized username + /// + /// Authorized username + [HttpGet] + public Task>> Username() + { + var response = new ScalarResponse(); + + var username = HttpContext.User.FindFirst(ClaimTypes.Name).Value; + response.WithItem(username); + + var result = new JsonResult(response) + { + ContentType = MediaTypeNames.Application.Json, + StatusCode = (int)(response.Success ? HttpStatusCode.OK : HttpStatusCode.InternalServerError) + }; + + return Task.FromResult>>(result); + } + + /// + /// Reads posts from https://jsonplaceholder.typicode.com/posts and converts to view entity + /// + /// Cancellation token + /// Post view entities + [HttpGet("List")] + [SuppressMessage("Analysis", "CA1031", Justification = "desired behavior")] + public async Task>> FakePost(CancellationToken token) + { + var response = new CollectionResponse(); + + try + { + RestResponse restResponse; + + using (var client = new RestClient()) + { + restResponse = await client + .ExecuteAsync(new RestRequest("https://jsonplaceholder.typicode.com/posts", Method.Get), token) + .ConfigureAwait(false); + } + + if (restResponse.Content == null) + { + response.WithError("Response body is empty"); + } + else + { + var items = _jsonSerializer.DeserializeObject(restResponse.Content); + var viewItems = items.Select(_dataContainersProvider.ToViewEntity).ToArray(); + + response.WithItems(viewItems); + } + } + catch (Exception exception) + { + response.WithError(exception); + } + + var result = new JsonResult(response) + { + ContentType = MediaTypeNames.Application.Json, + StatusCode = (int)(response.Success ? HttpStatusCode.OK : HttpStatusCode.InternalServerError) + }; + + return result; + } + + private class Post + { + public int Id { get; set; } + + public int UserId { get; set; } + + public string Title { get; set; } = null!; + + public string Body { get; set; } = null!; + } + } +} \ No newline at end of file diff --git a/tests/Tests/WebApplication.Test/Properties/AssemblyInfo.cs b/tests/Tests/WebApplication.Test/Properties/AssemblyInfo.cs new file mode 100644 index 00000000..31adb06d --- /dev/null +++ b/tests/Tests/WebApplication.Test/Properties/AssemblyInfo.cs @@ -0,0 +1,5 @@ +using System.Reflection; + +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] +[assembly: AssemblyInformationalVersion("1.0.0.0")] \ No newline at end of file diff --git a/tests/Tests/WebApplication.Test/WebApiTest.cs b/tests/Tests/WebApplication.Test/WebApiTest.cs new file mode 100644 index 00000000..2b0ad44d --- /dev/null +++ b/tests/Tests/WebApplication.Test/WebApiTest.cs @@ -0,0 +1,267 @@ +namespace SpaceEngineers.Core.WebApplication.Test +{ + using System; + using System.Collections.Generic; + using System.IdentityModel.Tokens.Jwt; + using System.Linq; + using System.Net; + using System.Threading; + using System.Threading.Tasks; + using AuthEndpoint.Host; + using Basics; + using Basics.Primitives; + using Core.Test.Api; + using Core.Test.Api.ClassFixtures; + using Core.Test.WebApplication; + using GenericEndpoint.Authorization.Host; + using GenericEndpoint.Authorization.Web; + using JwtAuthentication; + using Microsoft.Extensions.Configuration; + using Microsoft.Extensions.Hosting; + using Newtonsoft.Json; + using Newtonsoft.Json.Linq; + using RestSharp; + using Web.Api; + using Xunit; + using Xunit.Abstractions; + using Program = Core.Test.WebApplication.Program; + + /// + /// WebApiTest + /// + public class WebApiTest : TestBase + { + /// .cctor + /// ITestOutputHelper + /// TestFixture + public WebApiTest(ITestOutputHelper output, TestFixture fixture) + : base(output, fixture) + { + } + + /// + /// WebControllerTestData + /// + /// Test data + public static IEnumerable WebControllerTestData() + { + var hosts = WebControllerTestHosts().ToArray(); + var testCases = WebControllerTestCases().ToArray(); + var countdownEvent = new AsyncCountdownEvent(testCases.Length); + + return hosts + .SelectMany(host => testCases + .Select(testCase => host + .Concat(new object[] { countdownEvent }) + .Concat(testCase) + .ToArray())); + } + + internal static IEnumerable WebControllerTestHosts() + { + var timeout = TimeSpan.FromSeconds(60); + + var cts = new CancellationTokenSource(timeout); + + var host = new Lazy(() => + { + var solutionFileDirectory = SolutionExtensions.SolutionFile().Directory + ?? throw new InvalidOperationException("Solution directory wasn't found"); + + var settingsDirectory = solutionFileDirectory + .StepInto("tests") + .StepInto("Tests") + .StepInto("Test.WebApplication") + .StepInto("Settings"); + + var host = Program.BuildHost(settingsDirectory, Array.Empty()); + + host.StartAsync(cts.Token).Wait(cts.Token); + + return host; + }, + LazyThreadSafetyMode.ExecutionAndPublication); + + yield return new object[] { host, cts }; + } + + internal static IEnumerable WebControllerTestCases() + { + var username = "qwerty"; + var password = "12345678"; + + var solutionFileDirectory = SolutionExtensions.SolutionFile().Directory + ?? throw new InvalidOperationException("Solution directory wasn't found"); + + var appSettings = solutionFileDirectory + .StepInto("tests") + .StepInto("Tests") + .StepInto("Test.WebApplication") + .StepInto("Settings") + .GetFile("appsettings", ".json"); + + var authEndpointConfiguration = new ConfigurationBuilder() + .AddJsonFile(appSettings.FullName) + .Build(); + + var tokenProvider = new JwtTokenProvider(new JwtSecurityTokenHandler(), authEndpointConfiguration.GetJwtAuthenticationConfiguration()); + + /* + * Authorization = authentication + permissions + * 1. Authenticate user; call IdentityProvider and receive identity token; + * 2. Call AuthorizationProvider and receive API/Service/UI-specific roles and permissions; + * + * AuthorizationProvider maps identity data (who) to authorization data (permission and roles - what principal can do) + * Mappings can be static (common rules, conditions and conventions) or dynamic (claims transformations) + * Each application (web-api endpoint, micro-service, UI application, etc.) should have their scoped roles, permissions and mapping rules. + * Application can register authorization client so as to ask for authorization data from authorization service. + * For dynamically defined policies we can use custom PolicyProvider for policy-based authorization (ASP.NET CORE) and inject policy name into AuthorizeAttribute. + */ + + yield return new object?[] + { + new RestRequest($"http://127.0.0.1:5000/api/Auth/{nameof(AuthController.AuthenticateUser)}", Method.Get) + .AddHeader("Authorization", $"Basic {(username, password).EncodeBasicAuth()}"), + new Action(static (response, output) => + { + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.NotNull(response.Content); + output.WriteLine(response.Content); + + var token = JsonConvert + .DeserializeObject(response.Content) + ?.Property(nameof(ScalarResponse.Item), StringComparison.OrdinalIgnoreCase) + ?.Value + .ToString(); + Assert.NotNull(token); + var tokenPartsCount = token + .Split(' ', StringSplitOptions.RemoveEmptyEntries) + .SelectMany(part => part.Split('.', StringSplitOptions.RemoveEmptyEntries)) + .Count(); + Assert.Equal(3, tokenPartsCount); + }) + }; + + yield return new object?[] + { + new RestRequest($"http://127.0.0.1:5000/api/Auth/{nameof(AuthController.AuthenticateUser)}", Method.Get) + .AddHeader("Authorization", $"Bearer {tokenProvider.GenerateToken(username, new[] { AuthEndpoint.Contract.Features.Authentication }, TimeSpan.FromMinutes(1))}"), + new Action(static (response, output) => + { + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.NotNull(response.Content); + output.WriteLine(response.Content); + + var token = JsonConvert + .DeserializeObject(response.Content) + ?.Property(nameof(ScalarResponse.Item), StringComparison.OrdinalIgnoreCase) + ?.Value + .ToString(); + Assert.NotNull(token); + var tokenPartsCount = token + .Split(' ', StringSplitOptions.RemoveEmptyEntries) + .SelectMany(part => part.Split('.', StringSplitOptions.RemoveEmptyEntries)) + .Count(); + Assert.Equal(3, tokenPartsCount); + }) + }; + + yield return new object?[] + { + new RestRequest($"http://127.0.0.1:5000/api/Test/{nameof(TestController.ApplicationInfo)}", Method.Get), + new Action(static (response, output) => + { + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.NotNull(response.Content); + output.WriteLine(response.Content); + }) + }; + + yield return new object?[] + { + new RestRequest($"http://127.0.0.1:5000/api/Test/{nameof(TestController.Username)}", Method.Get) + .AddHeader("Authorization", $"Basic {(username, password).EncodeBasicAuth()}"), + new Action(static (response, output) => + { + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.NotNull(response.Content); + output.WriteLine(response.Content); + }) + }; + + yield return new object?[] + { + // TODO: #217 - review web.api serialization + new RestRequest($"http://127.0.0.1:5000/api/Test/{nameof(TestController.FakePost)}/List", Method.Get) + .AddHeader("Authorization", $"Bearer {tokenProvider.GenerateToken(username, new[] { Features.WebApiTest }, TimeSpan.FromMinutes(1))}"), + new Action(static (response, output) => + { + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.NotNull(response.Content); + output.WriteLine(response.Content); + }) + }; + } + + [Theory(Timeout = 60_000)] + [MemberData(nameof(WebControllerTestData))] + internal async Task WebControllerTest( + Lazy host, + CancellationTokenSource cts, + AsyncCountdownEvent asyncCountdownEvent, + RestRequest request, + Action assert) + { + try + { + Output.WriteLine(request.Resource); + + var hostShutdown = host.Value.WaitForShutdownAsync(cts.Token); + + using (var client = new RestClient()) + { + request.AddHeader("Cache-Control", "no-cache"); + + request.Timeout = TestCase.Timeout; + + var awaiter = Task.WhenAny( + hostShutdown, + client.GetAsync(request, cts.Token)); + + var result = await awaiter.ConfigureAwait(false); + + if (hostShutdown == result) + { + throw new InvalidOperationException("Host was unexpectedly stopped"); + } + + var response = await ((Task)result).ConfigureAwait(false); + + assert(response, Output); + } + } + finally + { + asyncCountdownEvent.Decrement(); + + if (asyncCountdownEvent.Read() == 0) + { + Output.WriteLine("CLEANUP"); + + try + { + await host + .Value + .StopAsync(cts.Token) + .ConfigureAwait(false); + } + finally + { + cts.Dispose(); + host.Value.Dispose(); + } + } + } + } + } +} \ No newline at end of file diff --git a/tests/Tests/WebApplication.Test/WebApplication.Test.csproj b/tests/Tests/WebApplication.Test/WebApplication.Test.csproj new file mode 100644 index 00000000..1f8b3e49 --- /dev/null +++ b/tests/Tests/WebApplication.Test/WebApplication.Test.csproj @@ -0,0 +1,30 @@ + + + + net7.0 + SpaceEngineers.Core.WebApplication.Test + SpaceEngineers.Core.WebApplication.Test + false + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + +