From 7f92f07423fcde0cb835d166356d4cca61ad78d4 Mon Sep 17 00:00:00 2001 From: ngfrolov Date: Fri, 3 Nov 2023 17:02:44 +0500 Subject: [PATCH 1/2] weekend test --- AsbCloudApp/Data/BackgroundWorkDto.cs | 2 +- .../Background/BackgroundWorker.cs | 76 +++++++++--- .../Background/PeriodicBackgroundWorker.cs | 113 ++++++++++++++++++ AsbCloudInfrastructure/Background/Work.cs | 12 +- .../Background/WorkStore.cs | 110 ----------------- .../DrillingProgram/DrillingProgramService.cs | 6 +- .../EmailNotificationTransportService.cs | 4 +- .../Services/ReportService.cs | 2 +- .../Services/SAUB/TelemetryDataCache.cs | 2 +- AsbCloudInfrastructure/Startup.cs | 12 +- .../UserConnectionsLimitMiddlwareTest.cs | 10 ++ .../Services/BackgroundWorkertest.cs | 93 ++++++++++++++ .../CrudServiceTestAbstract.cs | 0 .../DepositCrudCacheServiceTest.cs | 0 .../DetectedOperationServiceTest.cs | 0 .../DrillerServiceTest.cs | 0 .../DrillingProgramServiceTest.cs | 2 +- .../FileCategoryServiceTest.cs | 0 .../FileServiceTest.cs | 0 .../HelpPageServiceTest.cs | 0 .../LimitingParameterServiceTest.cs | 0 .../SAUB/TelemetryDataSaubCacheTests.cs | 0 .../TrajectoryVisualizationServiceTest.cs | 0 .../WellCompositeRepositoryTest.cs | 0 .../WellFinalDocumentsServiceTest.cs | 0 .../WellboreServiceTest.cs | 0 .../Controllers/BackgroundWorkController.cs | 27 ++--- .../PeriodicBackgroundWorkerController.cs | 44 +++++++ AsbCloudWebApi/SignalR/ReportsHub.cs | 2 +- .../SignalRNotificationTransportService.cs | 4 +- 30 files changed, 362 insertions(+), 159 deletions(-) create mode 100644 AsbCloudInfrastructure/Background/PeriodicBackgroundWorker.cs delete mode 100644 AsbCloudInfrastructure/Background/WorkStore.cs create mode 100644 AsbCloudWebApi.Tests/Services/BackgroundWorkertest.cs rename AsbCloudWebApi.Tests/{ServicesTests => Services}/CrudServiceTestAbstract.cs (100%) rename AsbCloudWebApi.Tests/{ServicesTests => Services}/DepositCrudCacheServiceTest.cs (100%) rename AsbCloudWebApi.Tests/{ServicesTests => Services}/DetectedOperationServiceTest.cs (100%) rename AsbCloudWebApi.Tests/{ServicesTests => Services}/DrillerServiceTest.cs (100%) rename AsbCloudWebApi.Tests/{ServicesTests => Services}/DrillingProgramServiceTest.cs (99%) rename AsbCloudWebApi.Tests/{ServicesTests => Services}/FileCategoryServiceTest.cs (100%) rename AsbCloudWebApi.Tests/{ServicesTests => Services}/FileServiceTest.cs (100%) rename AsbCloudWebApi.Tests/{ServicesTests => Services}/HelpPageServiceTest.cs (100%) rename AsbCloudWebApi.Tests/{ServicesTests => Services}/LimitingParameterServiceTest.cs (100%) rename AsbCloudWebApi.Tests/{ServicesTests => Services}/SAUB/TelemetryDataSaubCacheTests.cs (100%) rename AsbCloudWebApi.Tests/{ServicesTests => Services}/TrajectoryVisualizationServiceTest.cs (100%) rename AsbCloudWebApi.Tests/{ServicesTests => Services}/WellCompositeRepositoryTest.cs (100%) rename AsbCloudWebApi.Tests/{ServicesTests => Services}/WellFinalDocumentsServiceTest.cs (100%) rename AsbCloudWebApi.Tests/{ServicesTests => Services}/WellboreServiceTest.cs (100%) create mode 100644 AsbCloudWebApi/Controllers/PeriodicBackgroundWorkerController.cs diff --git a/AsbCloudApp/Data/BackgroundWorkDto.cs b/AsbCloudApp/Data/BackgroundWorkDto.cs index 9d4f2105..3374148c 100644 --- a/AsbCloudApp/Data/BackgroundWorkDto.cs +++ b/AsbCloudApp/Data/BackgroundWorkDto.cs @@ -172,7 +172,7 @@ namespace AsbCloudApp.Data if (progress.HasValue) CurrentState.Progress = progress.Value; - Trace.TraceInformation($"{WorkNameForTrace} state: {newState} [{100*progress:#}%]"); + Trace.TraceInformation($"{WorkNameForTrace} state[{100*progress:#}%]: {newState}"); } /// diff --git a/AsbCloudInfrastructure/Background/BackgroundWorker.cs b/AsbCloudInfrastructure/Background/BackgroundWorker.cs index 101a8273..beaca27e 100644 --- a/AsbCloudInfrastructure/Background/BackgroundWorker.cs +++ b/AsbCloudInfrastructure/Background/BackgroundWorker.cs @@ -1,7 +1,9 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using System; +using System.Collections.Generic; using System.Diagnostics; +using System.Linq; using System.Threading; using System.Threading.Tasks; @@ -12,13 +14,37 @@ namespace AsbCloudInfrastructure.Background; /// public class BackgroundWorker : BackgroundService { - private static readonly TimeSpan executePeriod = TimeSpan.FromSeconds(10); - private static readonly TimeSpan minDelay = TimeSpan.FromSeconds(2); + private readonly TimeSpan minDelay = TimeSpan.FromSeconds(1); private readonly IServiceProvider serviceProvider; - public WorkStore WorkStore { get; } = new WorkStore(); + /// + /// Очередь работ + /// + private Queue works = new(8); + + /// + /// Список периодических работ + /// + public IEnumerable Works => works; + + /// + /// Работа выполняемая в данный момент + /// public Work? CurrentWork; + /// + /// последние 16 завершившиеся с ошибкой + /// + public CyclycArray Felled { get; } = new(16); + + /// + /// последние 16 успешно завершенных + /// + public CyclycArray Done { get; } = new(16); + + /// + /// Ошибка в главном цикле, никогда не должна появляться + /// public string MainLoopLastException { get; private set; } = string.Empty; public BackgroundWorker(IServiceProvider serviceProvider) @@ -26,28 +52,31 @@ public class BackgroundWorker : BackgroundService this.serviceProvider = serviceProvider; } + /// + /// Добавить в очередь + /// + /// + public void Enqueue(Work work) + { + works.Enqueue(work); + if (ExecuteTask is null || ExecuteTask.IsCompleted) + StartAsync(CancellationToken.None).Wait(); + } + protected override async Task ExecuteAsync(CancellationToken token) { - while (!token.IsCancellationRequested) + while (!token.IsCancellationRequested && works.TryDequeue(out CurrentWork)) { try { - var work = WorkStore.GetNext(); - if (work is null) - { - await Task.Delay(executePeriod, token); - continue; - } - - CurrentWork = work; using var scope = serviceProvider.CreateScope(); - var result = await work.Start(scope.ServiceProvider, token); + var result = await CurrentWork.Start(scope.ServiceProvider, token); if (!result) - WorkStore.Felled.Add(work); + Felled.Add(CurrentWork); else - WorkStore.Done.Add(work); + Done.Add(CurrentWork); CurrentWork = null; await Task.Delay(minDelay, token); @@ -64,4 +93,21 @@ public class BackgroundWorker : BackgroundService } } } + + /// + /// Удаление работы по ID из одноразовой очереди + /// + /// + /// + public bool TryRemoveFromRunOnceQueue(string id) + { + var work = Works.FirstOrDefault(w => w.Id == id); + if (work is not null) + { + works = new Queue(Works.Where(w => w.Id != id)); + return true; + } + + return false; + } } diff --git a/AsbCloudInfrastructure/Background/PeriodicBackgroundWorker.cs b/AsbCloudInfrastructure/Background/PeriodicBackgroundWorker.cs new file mode 100644 index 00000000..fd12c13a --- /dev/null +++ b/AsbCloudInfrastructure/Background/PeriodicBackgroundWorker.cs @@ -0,0 +1,113 @@ +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +namespace AsbCloudInfrastructure.Background; + +/// +/// Сервис для фонового выполнения периодической работы +/// +public class PeriodicBackgroundWorker : BackgroundService +{ + private static readonly TimeSpan executePeriod = TimeSpan.FromSeconds(10); + private static readonly TimeSpan minDelay = TimeSpan.FromSeconds(1); + private readonly IServiceProvider serviceProvider; + + private readonly List works = new(8); + + /// + /// Список периодических работ + /// + public IEnumerable Works => works; + + /// + /// Работа выполняемая в данный момент + /// + public Work? CurrentWork; + + /// + /// Ошибка в главном цикле, никогда не должна появляться + /// + public string MainLoopLastException { get; private set; } = string.Empty; + + public PeriodicBackgroundWorker(IServiceProvider serviceProvider) + { + this.serviceProvider = serviceProvider; + } + + protected override async Task ExecuteAsync(CancellationToken token) + { + while (!token.IsCancellationRequested) + { + try + { + var periodicWork = GetNext(); + if (periodicWork is null) + { + await Task.Delay(executePeriod, token); + continue; + } + + CurrentWork = periodicWork.Work; + + using var scope = serviceProvider.CreateScope(); + + var result = await periodicWork.Work.Start(scope.ServiceProvider, token); + + CurrentWork = null; + await Task.Delay(minDelay, token); + } + catch (Exception ex) + { + MainLoopLastException = $"BackgroundWorker " + + $"MainLoopLastException: \r\n" + + $"date: {DateTime.Now:O}\r\n" + + $"message: {ex.Message}\r\n" + + $"inner: {ex.InnerException?.Message}\r\n" + + $"stackTrace: {ex.StackTrace}"; + Trace.TraceError(MainLoopLastException); + } + } + } + + /// + /// Добавить фоновую работу выполняющуюся с заданным периодом + /// + /// + /// + public void Add(TimeSpan period) + where T : Work, new() + { + var work = new T(); + var periodic = new WorkPeriodic(work, period); + works.Add(periodic); + } + + /// + /// Добавить фоновую работу выполняющуюся с заданным периодом + /// + /// + /// + public void Add(Work work, TimeSpan period) + { + var periodic = new WorkPeriodic(work, period); + works.Add(periodic); + } + + private WorkPeriodic? GetNext() + { + var work = works + .OrderBy(w => w.NextStart) + .FirstOrDefault(); + + if (work is null || work.NextStart > DateTime.Now) + return null; + + return work; + } +} diff --git a/AsbCloudInfrastructure/Background/Work.cs b/AsbCloudInfrastructure/Background/Work.cs index 4820aa26..1177892c 100644 --- a/AsbCloudInfrastructure/Background/Work.cs +++ b/AsbCloudInfrastructure/Background/Work.cs @@ -13,6 +13,8 @@ namespace AsbCloudInfrastructure.Background; /// public abstract class Work : BackgroundWorkDto { + private CancellationTokenSource? stoppingCts; + private sealed class WorkBase : Work { private Func, CancellationToken, Task> ActionAsync { get; } @@ -69,8 +71,9 @@ public abstract class Work : BackgroundWorkDto SetStatusStart(); try { - var task = Action(Id, services, UpdateStatus, token); - await task.WaitAsync(Timeout, token); + stoppingCts = CancellationTokenSource.CreateLinkedTokenSource(token); + var task = Action(Id, services, UpdateStatus, stoppingCts.Token); + await task.WaitAsync(Timeout, stoppingCts.Token); SetStatusComplete(); return true; } @@ -97,6 +100,11 @@ public abstract class Work : BackgroundWorkDto return false; } + public void Stop() + { + stoppingCts?.Cancel(); + } + private static string FormatExceptionMessage(Exception exception) { var firstException = FirstException(exception); diff --git a/AsbCloudInfrastructure/Background/WorkStore.cs b/AsbCloudInfrastructure/Background/WorkStore.cs deleted file mode 100644 index be7386e6..00000000 --- a/AsbCloudInfrastructure/Background/WorkStore.cs +++ /dev/null @@ -1,110 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; - -namespace AsbCloudInfrastructure.Background; - -/// -/// -/// Очередь работ -/// -/// Не периодические задачи будут возвращаться первыми, как самые приоритетные. -/// -public class WorkStore -{ - private readonly List periodics = new(8); - - /// - /// Список периодических задач - /// - public IEnumerable Periodics => periodics; - - /// - /// Работы выполняемые один раз - /// - public Queue RunOnceQueue { get; private set; } = new(8); - - /// - /// последние 16 завершившиеся с ошибкой - /// - public CyclycArray Felled { get; } = new(16); - - /// - /// последние 16 успешно завершенных - /// - public CyclycArray Done { get; } = new(16); - - /// - /// Добавить фоновую работу выполняющуюся с заданным периодом - /// - /// - /// - public void AddPeriodic(TimeSpan period) - where T : Work, new() - { - var work = new T(); - var periodic = new WorkPeriodic(work, period); - periodics.Add(periodic); - } - - /// - /// Добавить фоновую работу выполняющуюся с заданным периодом - /// - /// - /// - public void AddPeriodic(Work work, TimeSpan period) - { - var periodic = new WorkPeriodic(work, period); - periodics.Add(periodic); - } - - /// - /// Удаление работы по ID из одноразовой очереди - /// - /// - /// - public bool TryRemoveFromRunOnceQueue(string id) - { - var work = RunOnceQueue.FirstOrDefault(w => w.Id == id); - if (work is not null) - { - RunOnceQueue = new Queue(RunOnceQueue.Where(w => w.Id != id)); - return true; - } - - return false; - } - - /// - /// - /// Возвращает приоритетную задачу. - /// - /// - /// Если приоритетные закончились, то ищет ближайшую периодическую. - /// Если до старта ближайшей периодической работы меньше 20 сек, - /// то этой задаче устанавливается время последнего запуска в now и она возвращается. - /// Если больше 20 сек, то возвращается null. - /// - /// - /// - /// - public Work? GetNext() - { - if (RunOnceQueue.Any()) - return RunOnceQueue.Dequeue(); - - var work = GetNextPeriodic(); - if (work is null || work.NextStart > DateTime.Now) - return null; - - return work.Work; - } - - private WorkPeriodic? GetNextPeriodic() - { - var work = Periodics - .OrderBy(w => w.NextStart) - .FirstOrDefault(); - return work; - } -} diff --git a/AsbCloudInfrastructure/Services/DrillingProgram/DrillingProgramService.cs b/AsbCloudInfrastructure/Services/DrillingProgram/DrillingProgramService.cs index 228a3564..5557707d 100644 --- a/AsbCloudInfrastructure/Services/DrillingProgram/DrillingProgramService.cs +++ b/AsbCloudInfrastructure/Services/DrillingProgram/DrillingProgramService.cs @@ -513,7 +513,7 @@ namespace AsbCloudInfrastructure.Services.DrillingProgram if (state.IdState == idStateCreating) { var workId = MakeWorkId(idWell); - if (!backgroundWorker.WorkStore.RunOnceQueue.Any(w => w.Id == workId)) + if (!backgroundWorker.Works.Any(w => w.Id == workId)) { var well = (await wellService.GetOrDefaultAsync(idWell, token))!; var resultFileName = $"Программа бурения {well.Cluster} {well.Caption}.pdf"; @@ -542,7 +542,7 @@ namespace AsbCloudInfrastructure.Services.DrillingProgram var work = Work.CreateByDelegate(workId, workAction); work.OnErrorAsync = onErrorAction; - backgroundWorker.WorkStore.RunOnceQueue.Enqueue(work); + backgroundWorker.Enqueue(work); } } } @@ -556,7 +556,7 @@ namespace AsbCloudInfrastructure.Services.DrillingProgram private async Task RemoveDrillingProgramAsync(int idWell, CancellationToken token) { var workId = MakeWorkId(idWell); - backgroundWorker.WorkStore.TryRemoveFromRunOnceQueue(workId); + backgroundWorker.TryRemoveFromRunOnceQueue(workId); var filesIds = await context.Files .Where(f => f.IdWell == idWell && diff --git a/AsbCloudInfrastructure/Services/Email/EmailNotificationTransportService.cs b/AsbCloudInfrastructure/Services/Email/EmailNotificationTransportService.cs index bbbc8196..e56e423f 100644 --- a/AsbCloudInfrastructure/Services/Email/EmailNotificationTransportService.cs +++ b/AsbCloudInfrastructure/Services/Email/EmailNotificationTransportService.cs @@ -52,12 +52,12 @@ namespace AsbCloudInfrastructure.Services.Email } var workId = MakeWorkId(notification.IdUser, notification.Title, notification.Message); - if (!backgroundWorker.WorkStore.RunOnceQueue.Any(w=>w.Id==workId)) + if (!backgroundWorker.Works.Any(w=>w.Id==workId)) { var workAction = MakeEmailSendWorkAction(notification); var work = Work.CreateByDelegate(workId, workAction); - backgroundWorker.WorkStore.RunOnceQueue.Enqueue(work); + backgroundWorker.Enqueue(work); } return Task.CompletedTask; diff --git a/AsbCloudInfrastructure/Services/ReportService.cs b/AsbCloudInfrastructure/Services/ReportService.cs index 6a66c30e..ed24bc3e 100644 --- a/AsbCloudInfrastructure/Services/ReportService.cs +++ b/AsbCloudInfrastructure/Services/ReportService.cs @@ -95,7 +95,7 @@ namespace AsbCloudInfrastructure.Services }; var work = Work.CreateByDelegate(workId, workAction); - backgroundWorkerService.WorkStore.RunOnceQueue.Enqueue(work); + backgroundWorkerService.Enqueue(work); progressHandler.Invoke(new ReportProgressDto { diff --git a/AsbCloudInfrastructure/Services/SAUB/TelemetryDataCache.cs b/AsbCloudInfrastructure/Services/SAUB/TelemetryDataCache.cs index eabfcf77..3a6eac6f 100644 --- a/AsbCloudInfrastructure/Services/SAUB/TelemetryDataCache.cs +++ b/AsbCloudInfrastructure/Services/SAUB/TelemetryDataCache.cs @@ -52,7 +52,7 @@ namespace AsbCloudInfrastructure.Services.SAUB await instance.InitializeCacheFromDBAsync(db, onProgress, token); }); work.Timeout = TimeSpan.FromMinutes(15); - worker.WorkStore.RunOnceQueue.Enqueue(work); + worker.Enqueue(work); } return instance; } diff --git a/AsbCloudInfrastructure/Startup.cs b/AsbCloudInfrastructure/Startup.cs index cef6745b..64191e5e 100644 --- a/AsbCloudInfrastructure/Startup.cs +++ b/AsbCloudInfrastructure/Startup.cs @@ -33,12 +33,12 @@ namespace AsbCloudInfrastructure _ = provider.GetRequiredService>(); _ = provider.GetRequiredService>(); - var backgroundWorker = provider.GetRequiredService(); - backgroundWorker.WorkStore.AddPeriodic(TimeSpan.FromMinutes(30)); - backgroundWorker.WorkStore.AddPeriodic(TimeSpan.FromMinutes(15)); - backgroundWorker.WorkStore.AddPeriodic(TimeSpan.FromMinutes(30)); - backgroundWorker.WorkStore.AddPeriodic(TimeSpan.FromMinutes(30)); - backgroundWorker.WorkStore.AddPeriodic(MakeMemoryMonitoringWork(), TimeSpan.FromMinutes(1)); + var backgroundWorker = provider.GetRequiredService(); + backgroundWorker.Add(TimeSpan.FromMinutes(30)); + backgroundWorker.Add(TimeSpan.FromMinutes(15)); + backgroundWorker.Add(TimeSpan.FromMinutes(30)); + backgroundWorker.Add(TimeSpan.FromMinutes(30)); + backgroundWorker.Add(MakeMemoryMonitoringWork(), TimeSpan.FromMinutes(1)); var notificationBackgroundWorker = provider.GetRequiredService(); diff --git a/AsbCloudWebApi.Tests/Middlware/UserConnectionsLimitMiddlwareTest.cs b/AsbCloudWebApi.Tests/Middlware/UserConnectionsLimitMiddlwareTest.cs index 17a14ab1..7ced750b 100644 --- a/AsbCloudWebApi.Tests/Middlware/UserConnectionsLimitMiddlwareTest.cs +++ b/AsbCloudWebApi.Tests/Middlware/UserConnectionsLimitMiddlwareTest.cs @@ -53,11 +53,21 @@ namespace AsbCloudWebApi.Tests.Middlware throw new NotImplementedException(); } + public DatesRangeDto? GetRange(int idWell) + { + throw new NotImplementedException(); + } + public Task GetRangeAsync(int idWell, DateTimeOffset start, DateTimeOffset end, CancellationToken token) { throw new NotImplementedException(); } + public Task GetRangeAsync(int idWell, DateTimeOffset geDate, DateTimeOffset? leDate, CancellationToken token) + { + throw new NotImplementedException(); + } + public Task> GetTelemetryDataStatAsync(int idTelemetry, CancellationToken token) => throw new NotImplementedException(); public Task GetZippedCsv(int idWell, DateTime beginDate, DateTime endDate, CancellationToken token) diff --git a/AsbCloudWebApi.Tests/Services/BackgroundWorkertest.cs b/AsbCloudWebApi.Tests/Services/BackgroundWorkertest.cs new file mode 100644 index 00000000..a5690d84 --- /dev/null +++ b/AsbCloudWebApi.Tests/Services/BackgroundWorkertest.cs @@ -0,0 +1,93 @@ +using AsbCloudApp.Data; +using AsbCloudApp.Data.SAUB; +using AsbCloudApp.Repositories; +using AsbCloudApp.Requests; +using AsbCloudApp.Services; +using AsbCloudInfrastructure.Background; +using AsbCloudInfrastructure.Services; +using AsbCloudInfrastructure.Services.SAUB; +using Microsoft.Extensions.DependencyInjection; +using NSubstitute; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Threading; +using System.Threading.Tasks; +using Xunit; + +namespace AsbCloudWebApi.Tests.Services; + +public class BackgroundWorkertest +{ + private IServiceProvider provider; + private BackgroundWorker service; + + public BackgroundWorkertest() + { + provider = Substitute.For(); + var serviceScope = Substitute.For(); + var serviceScopeFactory = Substitute.For(); + serviceScopeFactory.CreateScope().Returns(serviceScope); + ((ISupportRequiredService)provider).GetRequiredService(typeof(IServiceScopeFactory)).Returns(serviceScopeFactory); + + service = new BackgroundWorker(provider); + typeof(BackgroundWorker) + .GetField("minDelay", BindingFlags.NonPublic | BindingFlags.Instance)? + .SetValue(service, TimeSpan.FromMilliseconds(1)); + } + + [Fact] + public async Task Enqueue_n_works() + { + var workCount = 10; + var result = 0; + Task workAction(string id, IServiceProvider services, Action callback, CancellationToken token) + { + result++; + return Task.Delay(1); + } + + //act + for (int i = 0; i < workCount; i++) + { + var work = Work.CreateByDelegate(i.ToString(), workAction); + service.Enqueue(work); + } + + var waitI = workCount; + await Task.Delay(1_000); + //while (waitI-- > 0 && service.ExecuteTask is not null && service.ExecuteTask.IsCompleted) + // await Task.Delay(4); + + //assert + Assert.Equal(workCount, result); + } + + [Fact] + public async Task Enqueue_continues_after_exceptions() + { + var expectadResult = 42; + var result = 0; + Task workAction(string id, IServiceProvider services, Action callback, CancellationToken token) + { + result = expectadResult; + return Task.CompletedTask; + } + + Task failAction(string id, IServiceProvider services, Action callback, CancellationToken token) + => throw new Exception(); + var goodWork = Work.CreateByDelegate("", workAction); + + var badWork = Work.CreateByDelegate("", failAction); + badWork.OnErrorAsync = (id, exception, token) => throw new Exception(); + + //act + service.Enqueue(badWork); + service.Enqueue(goodWork); + await Task.Delay(1200); + + //assert + Assert.Equal(expectadResult, result); + } +} diff --git a/AsbCloudWebApi.Tests/ServicesTests/CrudServiceTestAbstract.cs b/AsbCloudWebApi.Tests/Services/CrudServiceTestAbstract.cs similarity index 100% rename from AsbCloudWebApi.Tests/ServicesTests/CrudServiceTestAbstract.cs rename to AsbCloudWebApi.Tests/Services/CrudServiceTestAbstract.cs diff --git a/AsbCloudWebApi.Tests/ServicesTests/DepositCrudCacheServiceTest.cs b/AsbCloudWebApi.Tests/Services/DepositCrudCacheServiceTest.cs similarity index 100% rename from AsbCloudWebApi.Tests/ServicesTests/DepositCrudCacheServiceTest.cs rename to AsbCloudWebApi.Tests/Services/DepositCrudCacheServiceTest.cs diff --git a/AsbCloudWebApi.Tests/ServicesTests/DetectedOperationServiceTest.cs b/AsbCloudWebApi.Tests/Services/DetectedOperationServiceTest.cs similarity index 100% rename from AsbCloudWebApi.Tests/ServicesTests/DetectedOperationServiceTest.cs rename to AsbCloudWebApi.Tests/Services/DetectedOperationServiceTest.cs diff --git a/AsbCloudWebApi.Tests/ServicesTests/DrillerServiceTest.cs b/AsbCloudWebApi.Tests/Services/DrillerServiceTest.cs similarity index 100% rename from AsbCloudWebApi.Tests/ServicesTests/DrillerServiceTest.cs rename to AsbCloudWebApi.Tests/Services/DrillerServiceTest.cs diff --git a/AsbCloudWebApi.Tests/ServicesTests/DrillingProgramServiceTest.cs b/AsbCloudWebApi.Tests/Services/DrillingProgramServiceTest.cs similarity index 99% rename from AsbCloudWebApi.Tests/ServicesTests/DrillingProgramServiceTest.cs rename to AsbCloudWebApi.Tests/Services/DrillingProgramServiceTest.cs index f0ccd6c5..2a9635c9 100644 --- a/AsbCloudWebApi.Tests/ServicesTests/DrillingProgramServiceTest.cs +++ b/AsbCloudWebApi.Tests/Services/DrillingProgramServiceTest.cs @@ -366,7 +366,7 @@ namespace AsbCloudWebApi.Tests.ServicesTests var state = await service.GetStateAsync(idWell, publisher1.Id, CancellationToken.None); Assert.Equal(2, state.IdState); - backgroundWorkerMock.Verify(s => s.WorkStore.RunOnceQueue.Enqueue(It.IsAny())); + backgroundWorkerMock.Verify(s => s.Enqueue(It.IsAny())); } [Fact] diff --git a/AsbCloudWebApi.Tests/ServicesTests/FileCategoryServiceTest.cs b/AsbCloudWebApi.Tests/Services/FileCategoryServiceTest.cs similarity index 100% rename from AsbCloudWebApi.Tests/ServicesTests/FileCategoryServiceTest.cs rename to AsbCloudWebApi.Tests/Services/FileCategoryServiceTest.cs diff --git a/AsbCloudWebApi.Tests/ServicesTests/FileServiceTest.cs b/AsbCloudWebApi.Tests/Services/FileServiceTest.cs similarity index 100% rename from AsbCloudWebApi.Tests/ServicesTests/FileServiceTest.cs rename to AsbCloudWebApi.Tests/Services/FileServiceTest.cs diff --git a/AsbCloudWebApi.Tests/ServicesTests/HelpPageServiceTest.cs b/AsbCloudWebApi.Tests/Services/HelpPageServiceTest.cs similarity index 100% rename from AsbCloudWebApi.Tests/ServicesTests/HelpPageServiceTest.cs rename to AsbCloudWebApi.Tests/Services/HelpPageServiceTest.cs diff --git a/AsbCloudWebApi.Tests/ServicesTests/LimitingParameterServiceTest.cs b/AsbCloudWebApi.Tests/Services/LimitingParameterServiceTest.cs similarity index 100% rename from AsbCloudWebApi.Tests/ServicesTests/LimitingParameterServiceTest.cs rename to AsbCloudWebApi.Tests/Services/LimitingParameterServiceTest.cs diff --git a/AsbCloudWebApi.Tests/ServicesTests/SAUB/TelemetryDataSaubCacheTests.cs b/AsbCloudWebApi.Tests/Services/SAUB/TelemetryDataSaubCacheTests.cs similarity index 100% rename from AsbCloudWebApi.Tests/ServicesTests/SAUB/TelemetryDataSaubCacheTests.cs rename to AsbCloudWebApi.Tests/Services/SAUB/TelemetryDataSaubCacheTests.cs diff --git a/AsbCloudWebApi.Tests/ServicesTests/TrajectoryVisualizationServiceTest.cs b/AsbCloudWebApi.Tests/Services/TrajectoryVisualizationServiceTest.cs similarity index 100% rename from AsbCloudWebApi.Tests/ServicesTests/TrajectoryVisualizationServiceTest.cs rename to AsbCloudWebApi.Tests/Services/TrajectoryVisualizationServiceTest.cs diff --git a/AsbCloudWebApi.Tests/ServicesTests/WellCompositeRepositoryTest.cs b/AsbCloudWebApi.Tests/Services/WellCompositeRepositoryTest.cs similarity index 100% rename from AsbCloudWebApi.Tests/ServicesTests/WellCompositeRepositoryTest.cs rename to AsbCloudWebApi.Tests/Services/WellCompositeRepositoryTest.cs diff --git a/AsbCloudWebApi.Tests/ServicesTests/WellFinalDocumentsServiceTest.cs b/AsbCloudWebApi.Tests/Services/WellFinalDocumentsServiceTest.cs similarity index 100% rename from AsbCloudWebApi.Tests/ServicesTests/WellFinalDocumentsServiceTest.cs rename to AsbCloudWebApi.Tests/Services/WellFinalDocumentsServiceTest.cs diff --git a/AsbCloudWebApi.Tests/ServicesTests/WellboreServiceTest.cs b/AsbCloudWebApi.Tests/Services/WellboreServiceTest.cs similarity index 100% rename from AsbCloudWebApi.Tests/ServicesTests/WellboreServiceTest.cs rename to AsbCloudWebApi.Tests/Services/WellboreServiceTest.cs diff --git a/AsbCloudWebApi/Controllers/BackgroundWorkController.cs b/AsbCloudWebApi/Controllers/BackgroundWorkController.cs index 0a8c02c0..bb0cdc43 100644 --- a/AsbCloudWebApi/Controllers/BackgroundWorkController.cs +++ b/AsbCloudWebApi/Controllers/BackgroundWorkController.cs @@ -14,23 +14,22 @@ namespace AsbCloudWebApi.Controllers [ApiController] public class BackgroundWorkController : ControllerBase { - private readonly BackgroundWorker backgroundWorker; + private readonly BackgroundWorker worker; - public BackgroundWorkController(BackgroundWorker backgroundWorker) + public BackgroundWorkController(BackgroundWorker worker) { - this.backgroundWorker = backgroundWorker; + this.worker = worker; } [HttpGet] public IActionResult GetAll() { var result = new { - CurrentWork = (BackgroundWorkDto?)backgroundWorker.CurrentWork, - backgroundWorker.MainLoopLastException, - RunOnceQueue = backgroundWorker.WorkStore.RunOnceQueue.Select(work => (BackgroundWorkDto)work), - Periodics = backgroundWorker.WorkStore.Periodics.Select(work => (BackgroundWorkDto)work.Work), - Done = backgroundWorker.WorkStore.Done.Select(work => (BackgroundWorkDto)work), - Felled = backgroundWorker.WorkStore.Felled.Select(work => (BackgroundWorkDto)work), + CurrentWork = (BackgroundWorkDto?)worker.CurrentWork, + worker.MainLoopLastException, + RunOnceQueue = worker.Works.Select(work => (BackgroundWorkDto)work), + Done = worker.Done.Select(work => (BackgroundWorkDto)work), + Felled = worker.Felled.Select(work => (BackgroundWorkDto)work), }; return Ok(result); } @@ -38,7 +37,7 @@ namespace AsbCloudWebApi.Controllers [HttpGet("current")] public IActionResult GetCurrent() { - var work = backgroundWorker.CurrentWork; + var work = worker.CurrentWork; if (work == null) return NoContent(); @@ -48,22 +47,22 @@ namespace AsbCloudWebApi.Controllers [HttpGet("failed")] public IActionResult GetFelled() { - var result = backgroundWorker.WorkStore.Felled.Select(work => (BackgroundWorkDto)work); + var result = worker.Felled.Select(work => (BackgroundWorkDto)work); return Ok(result); } [HttpGet("done")] public IActionResult GetDone() { - var result = backgroundWorker.WorkStore.Done.Select(work => (BackgroundWorkDto)work); + var result = worker.Done.Select(work => (BackgroundWorkDto)work); return Ok(result); } [HttpPost("restart"), Obsolete("temporary method")] public async Task RestartAsync(CancellationToken token) { - await backgroundWorker.StopAsync(token); - await backgroundWorker.StartAsync(token); + await worker.StopAsync(token); + await worker.StartAsync(token); return Ok(); } } diff --git a/AsbCloudWebApi/Controllers/PeriodicBackgroundWorkerController.cs b/AsbCloudWebApi/Controllers/PeriodicBackgroundWorkerController.cs new file mode 100644 index 00000000..d7f666a0 --- /dev/null +++ b/AsbCloudWebApi/Controllers/PeriodicBackgroundWorkerController.cs @@ -0,0 +1,44 @@ +using AsbCloudApp.Data; +using AsbCloudInfrastructure.Background; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using System; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +namespace AsbCloudWebApi.Controllers +{ + [Route("api/[controller]")] + [Authorize] + [ApiController] + public class PeriodicBackgroundWorkerController : ControllerBase + { + private readonly PeriodicBackgroundWorker worker; + + public PeriodicBackgroundWorkerController(PeriodicBackgroundWorker worker) + { + this.worker = worker; + } + + [HttpGet] + public IActionResult GetAll() + { + var result = new + { + currentWork = (BackgroundWorkDto?)worker.CurrentWork, + worker.MainLoopLastException, + works = worker.Works.Select(work => (BackgroundWorkDto)work.Work), + }; + return Ok(result); + } + + [HttpPost("restart"), Obsolete("temporary method")] + public async Task RestartAsync(CancellationToken token) + { + await worker.StopAsync(token); + await worker.StartAsync(token); + return Ok(); + } + } +} diff --git a/AsbCloudWebApi/SignalR/ReportsHub.cs b/AsbCloudWebApi/SignalR/ReportsHub.cs index 1097c5ef..66292d05 100644 --- a/AsbCloudWebApi/SignalR/ReportsHub.cs +++ b/AsbCloudWebApi/SignalR/ReportsHub.cs @@ -26,7 +26,7 @@ namespace AsbCloudWebApi.SignalR await base.AddToGroup(groupName); var workId = groupName.Replace("Report_", ""); - var work = backgroundWorker.WorkStore.RunOnceQueue.FirstOrDefault(work => work.Id == workId); + var work = backgroundWorker.Works.FirstOrDefault(work => work.Id == workId); var progress = new ReportProgressDto() { diff --git a/AsbCloudWebApi/SignalR/Services/SignalRNotificationTransportService.cs b/AsbCloudWebApi/SignalR/Services/SignalRNotificationTransportService.cs index 962e5e0f..899d4ad7 100644 --- a/AsbCloudWebApi/SignalR/Services/SignalRNotificationTransportService.cs +++ b/AsbCloudWebApi/SignalR/Services/SignalRNotificationTransportService.cs @@ -29,12 +29,12 @@ public class SignalRNotificationTransportService : INotificationTransportService { var workId = HashCode.Combine(notifications.Select(n => n.Id)).ToString("x"); - if (backgroundWorker.WorkStore.RunOnceQueue.Any(w => w.Id == workId)) + if (backgroundWorker.Works.Any(w => w.Id == workId)) return Task.CompletedTask; var workAction = MakeSignalRSendWorkAction(notifications); var work = Work.CreateByDelegate(workId, workAction); - backgroundWorker.WorkStore.RunOnceQueue.Enqueue(work); + backgroundWorker.Enqueue(work); return Task.CompletedTask; } From 68d3d2724cfbd03309d6e0e379ccec7dcdacdc41 Mon Sep 17 00:00:00 2001 From: ngfrolov Date: Tue, 7 Nov 2023 14:19:13 +0500 Subject: [PATCH 2/2] Add Tests --- .../Background/BackgroundWorker.cs | 28 ++-- .../Background/PeriodicBackgroundWorker.cs | 6 +- AsbCloudInfrastructure/DependencyInjection.cs | 1 + .../DrillingProgram/DrillingProgramService.cs | 2 +- .../Services/BackgroundWorkertest.cs | 60 ++++++--- .../Services/PeriodicBackgroundWorkerTest.cs | 97 ++++++++++++++ AsbCloudWebApi.Tests/Services/WorkTest.cs | 126 ++++++++++++++++++ 7 files changed, 285 insertions(+), 35 deletions(-) create mode 100644 AsbCloudWebApi.Tests/Services/PeriodicBackgroundWorkerTest.cs create mode 100644 AsbCloudWebApi.Tests/Services/WorkTest.cs diff --git a/AsbCloudInfrastructure/Background/BackgroundWorker.cs b/AsbCloudInfrastructure/Background/BackgroundWorker.cs index beaca27e..f56b0ae4 100644 --- a/AsbCloudInfrastructure/Background/BackgroundWorker.cs +++ b/AsbCloudInfrastructure/Background/BackgroundWorker.cs @@ -52,17 +52,6 @@ public class BackgroundWorker : BackgroundService this.serviceProvider = serviceProvider; } - /// - /// Добавить в очередь - /// - /// - public void Enqueue(Work work) - { - works.Enqueue(work); - if (ExecuteTask is null || ExecuteTask.IsCompleted) - StartAsync(CancellationToken.None).Wait(); - } - protected override async Task ExecuteAsync(CancellationToken token) { while (!token.IsCancellationRequested && works.TryDequeue(out CurrentWork)) @@ -94,12 +83,27 @@ public class BackgroundWorker : BackgroundService } } + /// + /// Добавить в очередь + /// + /// work.Id может быть не уникальным, + /// при этом метод TryRemoveFromQueue удалит все работы с совпадающими id + /// + /// + /// + public void Enqueue(Work work) + { + works.Enqueue(work); + if (ExecuteTask is null || ExecuteTask.IsCompleted) + StartAsync(CancellationToken.None).Wait(); + } + /// /// Удаление работы по ID из одноразовой очереди /// /// /// - public bool TryRemoveFromRunOnceQueue(string id) + public bool TryRemoveFromQueue(string id) { var work = Works.FirstOrDefault(w => w.Id == id); if (work is not null) diff --git a/AsbCloudInfrastructure/Background/PeriodicBackgroundWorker.cs b/AsbCloudInfrastructure/Background/PeriodicBackgroundWorker.cs index fd12c13a..a7490ed7 100644 --- a/AsbCloudInfrastructure/Background/PeriodicBackgroundWorker.cs +++ b/AsbCloudInfrastructure/Background/PeriodicBackgroundWorker.cs @@ -14,8 +14,8 @@ namespace AsbCloudInfrastructure.Background; /// public class PeriodicBackgroundWorker : BackgroundService { - private static readonly TimeSpan executePeriod = TimeSpan.FromSeconds(10); - private static readonly TimeSpan minDelay = TimeSpan.FromSeconds(1); + private readonly TimeSpan executePeriod = TimeSpan.FromSeconds(10); + private readonly TimeSpan minDelay = TimeSpan.FromSeconds(1); private readonly IServiceProvider serviceProvider; private readonly List works = new(8); @@ -97,6 +97,8 @@ public class PeriodicBackgroundWorker : BackgroundService { var periodic = new WorkPeriodic(work, period); works.Add(periodic); + if (ExecuteTask is null || ExecuteTask.IsCompleted) + StartAsync(CancellationToken.None).Wait(); } private WorkPeriodic? GetNext() diff --git a/AsbCloudInfrastructure/DependencyInjection.cs b/AsbCloudInfrastructure/DependencyInjection.cs index 93c6575e..28bde671 100644 --- a/AsbCloudInfrastructure/DependencyInjection.cs +++ b/AsbCloudInfrastructure/DependencyInjection.cs @@ -165,6 +165,7 @@ namespace AsbCloudInfrastructure services.AddSingleton>(provider => TelemetryDataCache.GetInstance(provider)); services.AddSingleton>(provider => TelemetryDataCache.GetInstance(provider)); services.AddSingleton(); + services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(provider => ReduceSamplingService.GetInstance(configuration)); diff --git a/AsbCloudInfrastructure/Services/DrillingProgram/DrillingProgramService.cs b/AsbCloudInfrastructure/Services/DrillingProgram/DrillingProgramService.cs index 5557707d..c91689a3 100644 --- a/AsbCloudInfrastructure/Services/DrillingProgram/DrillingProgramService.cs +++ b/AsbCloudInfrastructure/Services/DrillingProgram/DrillingProgramService.cs @@ -556,7 +556,7 @@ namespace AsbCloudInfrastructure.Services.DrillingProgram private async Task RemoveDrillingProgramAsync(int idWell, CancellationToken token) { var workId = MakeWorkId(idWell); - backgroundWorker.TryRemoveFromRunOnceQueue(workId); + backgroundWorker.TryRemoveFromQueue(workId); var filesIds = await context.Files .Where(f => f.IdWell == idWell && diff --git a/AsbCloudWebApi.Tests/Services/BackgroundWorkertest.cs b/AsbCloudWebApi.Tests/Services/BackgroundWorkertest.cs index a5690d84..0f54dddf 100644 --- a/AsbCloudWebApi.Tests/Services/BackgroundWorkertest.cs +++ b/AsbCloudWebApi.Tests/Services/BackgroundWorkertest.cs @@ -1,16 +1,7 @@ -using AsbCloudApp.Data; -using AsbCloudApp.Data.SAUB; -using AsbCloudApp.Repositories; -using AsbCloudApp.Requests; -using AsbCloudApp.Services; -using AsbCloudInfrastructure.Background; -using AsbCloudInfrastructure.Services; -using AsbCloudInfrastructure.Services.SAUB; +using AsbCloudInfrastructure.Background; using Microsoft.Extensions.DependencyInjection; using NSubstitute; using System; -using System.Collections.Generic; -using System.Linq; using System.Reflection; using System.Threading; using System.Threading.Tasks; @@ -18,12 +9,12 @@ using Xunit; namespace AsbCloudWebApi.Tests.Services; -public class BackgroundWorkertest +public class BackgroundWorkerTest { private IServiceProvider provider; private BackgroundWorker service; - public BackgroundWorkertest() + public BackgroundWorkerTest() { provider = Substitute.For(); var serviceScope = Substitute.For(); @@ -33,7 +24,7 @@ public class BackgroundWorkertest service = new BackgroundWorker(provider); typeof(BackgroundWorker) - .GetField("minDelay", BindingFlags.NonPublic | BindingFlags.Instance)? + .GetField("minDelay", BindingFlags.NonPublic | BindingFlags.Instance) .SetValue(service, TimeSpan.FromMilliseconds(1)); } @@ -55,11 +46,8 @@ public class BackgroundWorkertest service.Enqueue(work); } - var waitI = workCount; - await Task.Delay(1_000); - //while (waitI-- > 0 && service.ExecuteTask is not null && service.ExecuteTask.IsCompleted) - // await Task.Delay(4); - + await service.ExecuteTask; + //assert Assert.Equal(workCount, result); } @@ -69,15 +57,16 @@ public class BackgroundWorkertest { var expectadResult = 42; var result = 0; + Task workAction(string id, IServiceProvider services, Action callback, CancellationToken token) { result = expectadResult; return Task.CompletedTask; } + var goodWork = Work.CreateByDelegate("", workAction); Task failAction(string id, IServiceProvider services, Action callback, CancellationToken token) => throw new Exception(); - var goodWork = Work.CreateByDelegate("", workAction); var badWork = Work.CreateByDelegate("", failAction); badWork.OnErrorAsync = (id, exception, token) => throw new Exception(); @@ -85,9 +74,40 @@ public class BackgroundWorkertest //act service.Enqueue(badWork); service.Enqueue(goodWork); - await Task.Delay(1200); + + await service.ExecuteTask; //assert Assert.Equal(expectadResult, result); + Assert.Equal(1, service.Felled.Count); + Assert.Equal(1, service.Done.Count); + } + + [Fact] + public async Task TryRemove() + { + var workCount = 5; + var result = 0; + Task workAction(string id, IServiceProvider services, Action callback, CancellationToken token) + { + result++; + return Task.Delay(10); + } + + //act + for (int i = 0; i < workCount; i++) + { + var work = Work.CreateByDelegate(i.ToString(), workAction); + service.Enqueue(work); + } + + var removed = service.TryRemoveFromQueue((workCount - 1).ToString()); + + await service.ExecuteTask; + + //assert + Assert.True(removed); + Assert.Equal(workCount - 1, result); + Assert.Equal(workCount - 1, service.Done.Count); } } diff --git a/AsbCloudWebApi.Tests/Services/PeriodicBackgroundWorkerTest.cs b/AsbCloudWebApi.Tests/Services/PeriodicBackgroundWorkerTest.cs new file mode 100644 index 00000000..d0c16285 --- /dev/null +++ b/AsbCloudWebApi.Tests/Services/PeriodicBackgroundWorkerTest.cs @@ -0,0 +1,97 @@ +using AsbCloudInfrastructure.Background; +using DocumentFormat.OpenXml.Drawing.Charts; +using Microsoft.Extensions.DependencyInjection; +using NSubstitute; +using System; +using System.Diagnostics; +using System.Reflection; +using System.Threading; +using System.Threading.Tasks; +using Xunit; + +namespace AsbCloudWebApi.Tests.Services; + +public class PeriodicBackgroundWorkerTest +{ + private IServiceProvider provider; + private PeriodicBackgroundWorker service; + + public PeriodicBackgroundWorkerTest() + { + provider = Substitute.For(); + var serviceScope = Substitute.For(); + var serviceScopeFactory = Substitute.For(); + serviceScopeFactory.CreateScope().Returns(serviceScope); + ((ISupportRequiredService)provider).GetRequiredService(typeof(IServiceScopeFactory)).Returns(serviceScopeFactory); + + service = new PeriodicBackgroundWorker(provider); + typeof(PeriodicBackgroundWorker) + .GetField("minDelay", BindingFlags.NonPublic | BindingFlags.Instance)? + .SetValue(service, TimeSpan.FromMilliseconds(1)); + + typeof(PeriodicBackgroundWorker) + .GetField("executePeriod", BindingFlags.NonPublic | BindingFlags.Instance)? + .SetValue(service, TimeSpan.FromMilliseconds(1)); + } + + [Fact] + public async Task WorkRunsTwice() + { + var workCount = 2; + var periodMs = 100d; + + var period = TimeSpan.FromMilliseconds(periodMs); + + var result = 0; + Task workAction(string id, IServiceProvider services, Action callback, CancellationToken token) + { + result++; + return Task.CompletedTask; + } + + //act + var work = Work.CreateByDelegate("", workAction); + + var stopwatch = Stopwatch.StartNew(); + service.Add(work, period); + + var delay = (periodMs / 20) + (periodMs * workCount) - stopwatch.ElapsedMilliseconds; + await Task.Delay(TimeSpan.FromMilliseconds(delay)); + + //assert + Assert.Equal(workCount, result); + } + + + [Fact] + public async Task Enqueue_continues_after_exceptions() + { + var expectadResult = 42; + var result = 0; + + Task workAction(string id, IServiceProvider services, Action callback, CancellationToken token) + { + result = expectadResult; + return Task.CompletedTask; + } + var goodWork = Work.CreateByDelegate("", workAction); + + Task failAction(string id, IServiceProvider services, Action callback, CancellationToken token) + => throw new Exception(); + + var badWork = Work.CreateByDelegate("", failAction); + badWork.OnErrorAsync = (id, exception, token) => throw new Exception(); + + //act + service.Add(badWork, TimeSpan.FromSeconds(2)); + service.Add(goodWork, TimeSpan.FromSeconds(2)); + + await Task.Delay(TimeSpan.FromMilliseconds(20)); + + //assert + Assert.Equal(expectadResult, result); + Assert.Equal(1, badWork.CountErrors); + Assert.Equal(1, goodWork.CountComplete); + Assert.Equal(1, goodWork.CountStart); + } +} diff --git a/AsbCloudWebApi.Tests/Services/WorkTest.cs b/AsbCloudWebApi.Tests/Services/WorkTest.cs new file mode 100644 index 00000000..ea826565 --- /dev/null +++ b/AsbCloudWebApi.Tests/Services/WorkTest.cs @@ -0,0 +1,126 @@ +using AsbCloudInfrastructure.Background; +using Microsoft.Extensions.DependencyInjection; +using NSubstitute; +using System; +using System.Threading; +using System.Threading.Tasks; +using Xunit; + +namespace AsbCloudWebApi.Tests.Services +{ + public class WorkTest + { + private IServiceProvider provider; + + public WorkTest() + { + provider = Substitute.For(); + var serviceScope = Substitute.For(); + var serviceScopeFactory = Substitute.For(); + serviceScopeFactory.CreateScope().Returns(serviceScope); + ((ISupportRequiredService)provider).GetRequiredService(typeof(IServiceScopeFactory)).Returns(serviceScopeFactory); + } + + [Fact] + public async Task Work_done_with_success() + { + Task workAction(string id, IServiceProvider services, Action callback, CancellationToken token) + => Task.CompletedTask; + + var work = Work.CreateByDelegate("", workAction); + + //act + var begin = DateTime.Now; + await work.Start(provider, CancellationToken.None); + var done = DateTime.Now; + var executionTime = done - begin; + + //assert + Assert.Equal(1, work.CountComplete); + Assert.Equal(1, work.CountStart); + Assert.Equal(0, work.CountErrors); + Assert.Null(work.CurrentState); + Assert.Null(work.LastError); + + var lastState = work.LastComplete; + Assert.NotNull(lastState); + Assert.InRange(lastState.Start, begin, done - 0.5 * executionTime); + Assert.InRange(lastState.End, done - 0.5 * executionTime, done); + Assert.InRange(lastState.ExecutionTime, TimeSpan.Zero, executionTime); + } + + [Fact] + public async Task Work_calls_callback() + { + var expectedState = "42"; + var expectedProgress = 42d; + + var timeout = TimeSpan.FromMilliseconds(40); + + Task workAction(string id, IServiceProvider services, Action callback, CancellationToken token) + { + callback.Invoke(expectedState, expectedProgress); + return Task.Delay(timeout); + } + + var work = Work.CreateByDelegate("", workAction); + + //act + var begin = DateTime.Now; + _ = work.Start(provider, CancellationToken.None); + await Task.Delay(timeout/3); + + //assert + Assert.Equal(0, work.CountComplete); + Assert.Equal(1, work.CountStart); + Assert.Equal(0, work.CountErrors); + Assert.NotNull(work.CurrentState); + Assert.Null(work.LastComplete); + Assert.Null(work.LastError); + + var currentState = work.CurrentState; + Assert.NotNull(currentState); + Assert.InRange(currentState.Start, begin, begin + timeout); + Assert.InRange(currentState.StateUpdate, begin, begin + timeout); + Assert.Equal(expectedState, currentState.State); + Assert.Equal(expectedProgress, currentState.Progress); + } + + + [Fact] + public async Task Work_fails_with_info() + { + var expectedState = "41"; + var expectedErrorText = "42"; + var minWorkTime = TimeSpan.FromMilliseconds(10); + + async Task workAction(string id, IServiceProvider services, Action callback, CancellationToken token) + { + await Task.Delay(minWorkTime); + callback(expectedState, 0); + throw new Exception(expectedErrorText); + } + + var work = Work.CreateByDelegate("", workAction); + + //act + var begin = DateTime.Now; + await work.Start(provider, CancellationToken.None); + + //assert + Assert.Equal(0, work.CountComplete); + Assert.Equal(1, work.CountStart); + Assert.Equal(1, work.CountErrors); + Assert.Null(work.CurrentState); + Assert.Null(work.LastComplete); + + var error = work.LastError; + Assert.NotNull(error); + Assert.InRange(error.Start, begin, DateTime.Now); + Assert.InRange(error.End, begin, DateTime.Now); + Assert.InRange(error.ExecutionTime, minWorkTime, DateTime.Now - begin); + Assert.Contains(expectedErrorText, error.ErrorText, StringComparison.InvariantCultureIgnoreCase); + Assert.Equal(expectedState, error.State); + } + } +}