From 5e6feba1b4791c46d221b07c0f182e4344c69169 Mon Sep 17 00:00:00 2001 From: karmeev Date: Sat, 9 Aug 2025 19:38:22 +0300 Subject: [PATCH 1/7] TEST(api): add unit tests in WorldMapServiceTests add 3 new unit tests in WorldMapServiceTests for detecting behavior in 3 different situations --- .../Fakes/FakeFuneral.cs | 21 +++ .../WorldMapServiceTests.cs | 131 +++++++++++++++++- 2 files changed, 148 insertions(+), 4 deletions(-) create mode 100644 src/nasa-server/tests/Nasa.Pathfinder.Services.Tests/Fakes/FakeFuneral.cs diff --git a/src/nasa-server/tests/Nasa.Pathfinder.Services.Tests/Fakes/FakeFuneral.cs b/src/nasa-server/tests/Nasa.Pathfinder.Services.Tests/Fakes/FakeFuneral.cs new file mode 100644 index 0000000..1655a45 --- /dev/null +++ b/src/nasa-server/tests/Nasa.Pathfinder.Services.Tests/Fakes/FakeFuneral.cs @@ -0,0 +1,21 @@ +using Bogus; +using Nasa.Pathfinder.Domain.Entities.World; +using Nasa.Pathfinder.Domain.World; + +namespace Nasa.Pathfinder.Services.Tests.Fakes; + +public static class FakeFuneral +{ + public static Funeral Make(string mapId) + { + return new Faker() + .RuleFor(x => x.Id, faker => faker.Database.Random.Uuid().ToString()) + .RuleFor(x => x.ETag, faker => faker.Random.Guid()) + .RuleFor(x => x.MapId, mapId) + .RuleFor(x => x.Value, _ => + new Faker() + .RuleFor(x => x.X, faker => faker.Random.Int()) + .RuleFor(x => x.Y, faker => faker.Random.Int()) + .RuleFor(x => x.Direction, faker => faker.Random.Enum())); + } +} \ No newline at end of file diff --git a/src/nasa-server/tests/Nasa.Pathfinder.Services.Tests/WorldMapServiceTests.cs b/src/nasa-server/tests/Nasa.Pathfinder.Services.Tests/WorldMapServiceTests.cs index 8f8e7de..ba61862 100644 --- a/src/nasa-server/tests/Nasa.Pathfinder.Services.Tests/WorldMapServiceTests.cs +++ b/src/nasa-server/tests/Nasa.Pathfinder.Services.Tests/WorldMapServiceTests.cs @@ -1,8 +1,11 @@ +using Bogus; using Moq; using Nasa.Pathfinder.Data.Contracts.Repositories; +using Nasa.Pathfinder.Domain.Entities.World; using Nasa.Pathfinder.Domain.Interactions; using Nasa.Pathfinder.Domain.World; using Nasa.Pathfinder.Services.Internal; +using Nasa.Pathfinder.Services.Tests.Fakes; using Nasa.Pathfinder.Tests; using NUnit.Framework; @@ -15,17 +18,17 @@ public class WorldMapServiceTests public void Setup() { _mockMapRepository = new Mock(); - _mockFuneralRepository = new Mock(); + _mockGraveRepository = new Mock(); } private Mock _mockMapRepository; - private Mock _mockFuneralRepository; + private Mock _mockGraveRepository; [Test] public void CalculateDesiredPosition_WhenWeGotListOfCommands_ShouldReturnsDestination() { TestRunner - .Arrange(() => new WorldMapService(_mockMapRepository.Object, _mockFuneralRepository.Object)) + .Arrange(() => new WorldMapService(_mockMapRepository.Object, _mockGraveRepository.Object)) .Act(sut => { var currentPosition = new Position @@ -62,7 +65,7 @@ public void CalculateDesiredPosition_WhenWeGotListOfCommands_ShouldReturnsDestin public void CalculateDesiredPosition_WhenDesiredPositionOutOfGrid_ShouldReturnsDestination() { TestRunner - .Arrange(() => new WorldMapService(_mockMapRepository.Object, _mockFuneralRepository.Object)) + .Arrange(() => new WorldMapService(_mockMapRepository.Object, _mockGraveRepository.Object)) .Act(sut => { var currentPosition = new Position @@ -97,4 +100,124 @@ public void CalculateDesiredPosition_WhenDesiredPositionOutOfGrid_ShouldReturnsD }); }); } + + [Test] + public async Task TryReachPosition_WhenFuneralWithDesiredPositionFindAndNotOutOfMap_ShouldReturnNotChanged() + { + var mapId = $"map_{new Faker().Database.Random.Uuid()}"; + + var desiredPosition = new Position + { + X = 1, + Y = 3, + Direction = Direction.W + }; + + await TestRunner + .Arrange(() => + { + _mockMapRepository.Setup(x => x.TryGetAsync(mapId, It.IsAny())) + .ReturnsAsync(new MapInfo + { + Id = mapId, + ETag = new Faker().Random.Guid(), + SizeX = 50, + SizeY = 50, + }); + + _mockGraveRepository.Setup(x => + x.GetFuneralsAsync(mapId, It.IsAny())) + .ReturnsAsync(new List + { + new() + { + Id = new Faker().Random.Guid().ToString(), + ETag = new Faker().Random.Guid(), + MapId = mapId, + Value = desiredPosition + } + }); + + return new WorldMapService(_mockMapRepository.Object, _mockGraveRepository.Object); + }) + .ActAsync(async sut => await sut.TryReachPosition(mapId, desiredPosition)) + .ThenAssertAsync(position => + { + Assert.That(position, Is.TypeOf()); + }); + } + + [Test] + public async Task TryReachPosition_WhenFuneralWithDesiredPositionOutOfMap_ShouldReturnNotChanged() + { + var mapId = $"map_{new Faker().Database.Random.Uuid()}"; + + var desiredPosition = new Position + { + X = 51, + Y = 3, + Direction = Direction.W + }; + + await TestRunner + .Arrange(() => + { + _mockMapRepository.Setup(x => x.TryGetAsync(mapId, It.IsAny())) + .ReturnsAsync(new MapInfo + { + Id = mapId, + ETag = new Faker().Random.Guid(), + SizeX = 50, + SizeY = 50, + }); + + _mockGraveRepository.Setup(x => + x.GetFuneralsAsync(mapId, It.IsAny())) + .ReturnsAsync(new List { FakeFuneral.Make(mapId) }); + + return new WorldMapService(_mockMapRepository.Object, _mockGraveRepository.Object); + }) + .ActAsync(async sut => await sut.TryReachPosition(mapId, desiredPosition)) + .ThenAssertAsync(position => + { + Assert.That(position, Is.TypeOf()); + }); + } + + [Test] + public async Task TryReachPosition_WhenBotCanMove_ShouldReturnChanged() + { + var mapId = $"map_{new Faker().Database.Random.Uuid()}"; + + var desiredPosition = new Position + { + X = 4, + Y = 3, + Direction = Direction.W + }; + + await TestRunner + .Arrange(() => + { + _mockMapRepository.Setup(x => x.TryGetAsync(mapId, It.IsAny())) + .ReturnsAsync(new MapInfo + { + Id = mapId, + ETag = new Faker().Random.Guid(), + SizeX = 50, + SizeY = 50, + }); + + _mockGraveRepository.Setup(x => + x.GetFuneralsAsync(mapId, It.IsAny())) + .ReturnsAsync(new List { FakeFuneral.Make(mapId) }); + + return new WorldMapService(_mockMapRepository.Object, _mockGraveRepository.Object); + }) + .ActAsync(async sut => await sut.TryReachPosition(mapId, desiredPosition)) + .ThenAssertAsync(position => + { + Assert.That(position, Is.TypeOf()); + }); + } } \ No newline at end of file From 0bcdb42e8a96e93f7a1738f50c10160d71907e52 Mon Sep 17 00:00:00 2001 From: karmeev Date: Sat, 9 Aug 2025 19:39:41 +0300 Subject: [PATCH 2/7] revert: restore previous name of variable --- .../WorldMapServiceTests.cs | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/nasa-server/tests/Nasa.Pathfinder.Services.Tests/WorldMapServiceTests.cs b/src/nasa-server/tests/Nasa.Pathfinder.Services.Tests/WorldMapServiceTests.cs index ba61862..384304b 100644 --- a/src/nasa-server/tests/Nasa.Pathfinder.Services.Tests/WorldMapServiceTests.cs +++ b/src/nasa-server/tests/Nasa.Pathfinder.Services.Tests/WorldMapServiceTests.cs @@ -18,17 +18,17 @@ public class WorldMapServiceTests public void Setup() { _mockMapRepository = new Mock(); - _mockGraveRepository = new Mock(); + _mockFuneralRepository = new Mock(); } private Mock _mockMapRepository; - private Mock _mockGraveRepository; + private Mock _mockFuneralRepository; [Test] public void CalculateDesiredPosition_WhenWeGotListOfCommands_ShouldReturnsDestination() { TestRunner - .Arrange(() => new WorldMapService(_mockMapRepository.Object, _mockGraveRepository.Object)) + .Arrange(() => new WorldMapService(_mockMapRepository.Object, _mockFuneralRepository.Object)) .Act(sut => { var currentPosition = new Position @@ -65,7 +65,7 @@ public void CalculateDesiredPosition_WhenWeGotListOfCommands_ShouldReturnsDestin public void CalculateDesiredPosition_WhenDesiredPositionOutOfGrid_ShouldReturnsDestination() { TestRunner - .Arrange(() => new WorldMapService(_mockMapRepository.Object, _mockGraveRepository.Object)) + .Arrange(() => new WorldMapService(_mockMapRepository.Object, _mockFuneralRepository.Object)) .Act(sut => { var currentPosition = new Position @@ -125,7 +125,7 @@ await TestRunner SizeY = 50, }); - _mockGraveRepository.Setup(x => + _mockFuneralRepository.Setup(x => x.GetFuneralsAsync(mapId, It.IsAny())) .ReturnsAsync(new List { @@ -138,7 +138,7 @@ await TestRunner } }); - return new WorldMapService(_mockMapRepository.Object, _mockGraveRepository.Object); + return new WorldMapService(_mockMapRepository.Object, _mockFuneralRepository.Object); }) .ActAsync(async sut => await sut.TryReachPosition(mapId, desiredPosition)) .ThenAssertAsync(position => @@ -171,11 +171,11 @@ await TestRunner SizeY = 50, }); - _mockGraveRepository.Setup(x => + _mockFuneralRepository.Setup(x => x.GetFuneralsAsync(mapId, It.IsAny())) .ReturnsAsync(new List { FakeFuneral.Make(mapId) }); - return new WorldMapService(_mockMapRepository.Object, _mockGraveRepository.Object); + return new WorldMapService(_mockMapRepository.Object, _mockFuneralRepository.Object); }) .ActAsync(async sut => await sut.TryReachPosition(mapId, desiredPosition)) .ThenAssertAsync(position => @@ -208,11 +208,11 @@ await TestRunner SizeY = 50, }); - _mockGraveRepository.Setup(x => + _mockFuneralRepository.Setup(x => x.GetFuneralsAsync(mapId, It.IsAny())) .ReturnsAsync(new List { FakeFuneral.Make(mapId) }); - return new WorldMapService(_mockMapRepository.Object, _mockGraveRepository.Object); + return new WorldMapService(_mockMapRepository.Object, _mockFuneralRepository.Object); }) .ActAsync(async sut => await sut.TryReachPosition(mapId, desiredPosition)) .ThenAssertAsync(position => From 3b58819df072cdb1b172904e25acd01d79f9ea0e Mon Sep 17 00:00:00 2001 From: karmeev Date: Sun, 10 Aug 2025 01:02:17 +0300 Subject: [PATCH 3/7] TEST(api): implement integration test for sending messages --- cd/stage/docker-compose.local.yaml | 6 + .../Internal/Memory/MemoryDataContext.cs | 12 +- .../Background/MigrationBackgroundTask.cs | 16 +- .../src/Nasa.Pathfinder/Hubs/MessageHub.cs | 5 +- .../Nasa.IntegrationTests.Pathfinder.csproj | 1 + .../PathfinderServiceTests.cs | 243 ++++++++++++++++-- tests/integration/integration-tests.sln | 5 + 7 files changed, 248 insertions(+), 40 deletions(-) create mode 100644 cd/stage/docker-compose.local.yaml diff --git a/cd/stage/docker-compose.local.yaml b/cd/stage/docker-compose.local.yaml new file mode 100644 index 0000000..f3609bd --- /dev/null +++ b/cd/stage/docker-compose.local.yaml @@ -0,0 +1,6 @@ +services: + nasa-server: + image: ghcr.io/karmeev/mars-pathfinder:stage + container_name: nasa-server + ports: + - "5000:5000" \ No newline at end of file diff --git a/src/nasa-server/src/Nasa.Pathfinder.Infrastructure/Internal/Memory/MemoryDataContext.cs b/src/nasa-server/src/Nasa.Pathfinder.Infrastructure/Internal/Memory/MemoryDataContext.cs index 86719b8..a3de2b7 100644 --- a/src/nasa-server/src/Nasa.Pathfinder.Infrastructure/Internal/Memory/MemoryDataContext.cs +++ b/src/nasa-server/src/Nasa.Pathfinder.Infrastructure/Internal/Memory/MemoryDataContext.cs @@ -40,14 +40,20 @@ public Task> UpdateAsync(T entry, CancellationToken ct = default) } } - public Task GetAsync(string id, CancellationToken ct = default) where T : class, IEntity + public async Task GetAsync(string id, CancellationToken ct = default) where T : class, IEntity { + await Task.CompletedTask; + var lockObj = GetEntityLock(id); lock (lockObj) { var result = Get(id); - if (result.IsError) return null; - return Task.FromResult(result.Value); + if (result.IsError) + return null; + + var entity = result.Value; + + return entity; } } diff --git a/src/nasa-server/src/Nasa.Pathfinder/Background/MigrationBackgroundTask.cs b/src/nasa-server/src/Nasa.Pathfinder/Background/MigrationBackgroundTask.cs index cd2e128..c1cac28 100644 --- a/src/nasa-server/src/Nasa.Pathfinder/Background/MigrationBackgroundTask.cs +++ b/src/nasa-server/src/Nasa.Pathfinder/Background/MigrationBackgroundTask.cs @@ -17,8 +17,8 @@ public async Task StartAsync(CancellationToken cancellationToken) { Id = mapId, ETag = Guid.NewGuid(), - SizeX = 50, - SizeY = 50, + SizeX = 5, + SizeY = 3, }; await context.PushAsync(mapInfo, cancellationToken); @@ -35,7 +35,7 @@ await context.PushAsync(new Bot { X = 1, Y = 1, - Direction = Direction.N + Direction = Direction.E } }, cancellationToken); @@ -49,8 +49,8 @@ await context.PushAsync(new Bot LastWords = "There is only the Emperor, and he is our shield and protector.", Position = new Position { - X = 10, - Y = 6, + X = 3, + Y = 2, Direction = Direction.N } }, cancellationToken); @@ -65,9 +65,9 @@ await context.PushAsync(new Bot LastWords = "Blessed is the mind too small for doubt.", Position = new Position { - X = 5, - Y = 5, - Direction = Direction.N + X = 0, + Y = 3, + Direction = Direction.W } }, cancellationToken); } diff --git a/src/nasa-server/src/Nasa.Pathfinder/Hubs/MessageHub.cs b/src/nasa-server/src/Nasa.Pathfinder/Hubs/MessageHub.cs index dc7ef70..2fd355e 100644 --- a/src/nasa-server/src/Nasa.Pathfinder/Hubs/MessageHub.cs +++ b/src/nasa-server/src/Nasa.Pathfinder/Hubs/MessageHub.cs @@ -48,11 +48,12 @@ private Task StartConsumer(ConcurrentQueue queue, Cancellati { if (queue.IsEmpty) { - await Task.Delay(20, token); + await Task.Delay(5, token); continue; } - if (queue.TryDequeue(out var message)) await NotifyClient(message); + if (queue.TryDequeue(out var message)) + await NotifyClient(message); } }, token); } diff --git a/tests/integration/Nasa.IntegrationTests.Pathfinder/Nasa.IntegrationTests.Pathfinder.csproj b/tests/integration/Nasa.IntegrationTests.Pathfinder/Nasa.IntegrationTests.Pathfinder.csproj index 62aae58..ffe9576 100644 --- a/tests/integration/Nasa.IntegrationTests.Pathfinder/Nasa.IntegrationTests.Pathfinder.csproj +++ b/tests/integration/Nasa.IntegrationTests.Pathfinder/Nasa.IntegrationTests.Pathfinder.csproj @@ -11,6 +11,7 @@ + diff --git a/tests/integration/Nasa.IntegrationTests.Pathfinder/PathfinderServiceTests.cs b/tests/integration/Nasa.IntegrationTests.Pathfinder/PathfinderServiceTests.cs index 1cba1c1..f4ca1f7 100644 --- a/tests/integration/Nasa.IntegrationTests.Pathfinder/PathfinderServiceTests.cs +++ b/tests/integration/Nasa.IntegrationTests.Pathfinder/PathfinderServiceTests.cs @@ -1,4 +1,5 @@ using System.Diagnostics; +using ErrorOr; using Google.Protobuf.WellKnownTypes; using Grpc.Core; using Grpc.Net.Client; @@ -177,45 +178,233 @@ public void ResetBot_BotIsFree_ShouldInvalidArgument() } [Test] - public async Task SendMessage_SendCommandAndWaitResult_ShouldReturnMessageWithBotStatus() + [Ignore("Right now, it's only for manual starting")] + public async Task SendMessage_ApplyTestTaskBehavior_ShouldReturn3DifferentMessages() { - await TestRunner - .Arrange(() => new Client(_channel)) + const string message1 = "RFRFRFRF"; + const string message2 = "FRRFLLFFRRFLL"; + const string message3 = "LLFFFLFLFL"; + var botsMessage = new Dictionary(); + var botsPositions = new Dictionary + { + { message1, new Position { X = 1, Y = 1, Direction = "E" } }, + { message2, new Position { X = 3, Y = 3, Direction = "N" } }, + { message3, new Position { X = 2, Y = 4, Direction = "S" } }, + }; + + await TestRunner>> + .Arrange(() => + { + var client = new Client(_channel); + return client; + }) .ActAsync(async sut => { - var bot = sut.GetBots(new Empty()).Bots.First(); - bot = sut.SelectBot(new SelectBotRequest - { - BotId = bot.Id - }).Bot; + var responses = new List>(); - var stream = sut.SendMessage(new Metadata + try { - { "TraceId", Activity.Current?.TraceId.ToString() ?? Guid.NewGuid().ToString() }, - { "clientId", Guid.NewGuid().ToString() } - }); + //<--- 3 Bots case ---> + //<--- First bot ---> - var cts = new CancellationTokenSource(); + var first = sut.GetBots(new Empty()).Bots.First(x => x.Status == "Available"); + var bot = sut.SelectBot(new SelectBotRequest + { + BotId = first.Id + }).Bot; - await stream.RequestStream.WriteAsync(new SendMessageRequest - { - BotId = bot.Id, - Message = "RFRFRFRF" - }, cts.Token); - - await stream.RequestStream.CompleteAsync(); + var stream = sut.SendMessage(new Metadata + { + { "TraceId", Activity.Current?.TraceId.ToString() ?? Guid.NewGuid().ToString() }, + { "clientId", Guid.NewGuid().ToString() } + }); + + Thread.Sleep(1000); + + botsMessage.Add(bot.Id, message1); + await stream.RequestStream.WriteAsync(new SendMessageRequest + { + BotId = bot.Id, + Message = message1 + }, CancellationToken.None); + + Thread.Sleep(2000); + + var cts = new CancellationTokenSource(); + var token = cts.Token; + + try + { + while (await stream.ResponseStream.MoveNext(token)) + { + var response = stream.ResponseStream.Current; + responses.Add(response); + break; + } + } + catch (Exception ex) + { + responses.Add(Error.Failure(ex.Message)); + } + + await stream.RequestStream.CompleteAsync(); + + //<--- Second bot ---> + + var second = sut.GetBots(new Empty()).Bots.First(x => x.Status == "Available"); + bot = sut.SelectBot(new SelectBotRequest + { + BotId = second.Id + }).Bot; + + stream = sut.SendMessage(new Metadata + { + { "TraceId", Activity.Current?.TraceId.ToString() ?? Guid.NewGuid().ToString() }, + { "clientId", Guid.NewGuid().ToString() } + }); + + Thread.Sleep(1000); + + botsMessage.Add(bot.Id, message2); + await stream.RequestStream.WriteAsync(new SendMessageRequest + { + BotId = bot.Id, + Message = message2 + }, CancellationToken.None); + + Thread.Sleep(2000); + + try + { + while (await stream.ResponseStream.MoveNext(token)) + { + var response = stream.ResponseStream.Current; + responses.Add(response); + break; + } + } + catch (Exception ex) + { + responses.Add(Error.Failure(ex.Message)); + } - while (await stream.ResponseStream.MoveNext(cts.Token)) + await stream.RequestStream.CompleteAsync(); + + //<--- Third bot ---> + + var third = sut.GetBots(new Empty()).Bots.First(x => x.Status == "Available"); + bot = sut.SelectBot(new SelectBotRequest + { + BotId = third.Id + }).Bot; + + stream = sut.SendMessage(new Metadata + { + { "TraceId", Activity.Current?.TraceId.ToString() ?? Guid.NewGuid().ToString() }, + { "clientId", Guid.NewGuid().ToString() } + }); + + Thread.Sleep(1000); + + botsMessage.Add(bot.Id, message3); + await stream.RequestStream.WriteAsync(new SendMessageRequest + { + BotId = bot.Id, + Message = message3 + }, CancellationToken.None); + + Thread.Sleep(2000); + + try + { + while (await stream.ResponseStream.MoveNext(token)) + { + var response = stream.ResponseStream.Current; + responses.Add(response); + break; + } + } + catch (Exception ex) + { + responses.Add(Error.Failure(ex.Message)); + } + + await stream.RequestStream.CompleteAsync(); + } + catch (Exception ex) { - var response = stream.ResponseStream.Current; - return response; + responses.Add(Error.Failure(ex.Message)); } - - throw new Exception("No response received from gRPC stream."); + + return responses; }) - .ThenAssertAsync(_ => + .ThenAssertAsync(responses => { - Assert.Pass(); + var errors = new List(); + + foreach (var response in responses) + { + if (response.IsError) + { + errors.Add(response.FirstError); + } + } + + try + { + var client = new Client(_channel); + var bots = client.GetBots(new Empty()).Bots; + + foreach (var bot in bots) + { + if (bot.Status == "Available") + errors.Add(Error.Unexpected("Bot is available", + $"Bot with id ({bot.Id}) is still available instead of be acquired")); + + var message = botsMessage[bot.Id]; + + var pattern = ""; + switch (message) + { + case message1: + pattern = message1; + break; + case message2: + pattern = message2; + break; + case message3: + pattern = message3; + if (bot.Status != "Dead") + { + errors.Add(Error.Unexpected("Incorrect status", + $"Bot with id ({bot.Id}) must dead, but it still alive!")); + } + break; + } + + if (bot.Position.X != botsPositions[pattern].X || bot.Position.Y != botsPositions[pattern].Y || + bot.Position.Direction != botsPositions[pattern].Direction) + { + errors.Add(Error.Unexpected("Incorrect position", + $"Bot with id ({bot.Id}) has incorrect position")); + } + } + + } + finally + { + if (!errors.Any()) Assert.Pass(); + + if (errors.Count > 0) + { + foreach (var error in errors) + { + Console.WriteLine("{0}: {1}", error.Code, error.Description); + } + + Assert.Fail(); + } + } }); } diff --git a/tests/integration/integration-tests.sln b/tests/integration/integration-tests.sln index bdc92b6..42be69e 100644 --- a/tests/integration/integration-tests.sln +++ b/tests/integration/integration-tests.sln @@ -4,6 +4,11 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Nasa.IntegrationTests", "Na EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Nasa.IntegrationTests.Pathfinder", "Nasa.IntegrationTests.Pathfinder\Nasa.IntegrationTests.Pathfinder.csproj", "{4879B4E7-BC2B-43DB-8CC5-5B26272939F3}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{697A6CE2-07F5-4DA4-A385-F2608C30E561}" + ProjectSection(SolutionItems) = preProject + ..\..\cd\stage\docker-compose.local.yaml = ..\..\cd\stage\docker-compose.local.yaml + EndProjectSection +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU From ce45c0236d3775779c7a1b0dc9f977b1ee3a283b Mon Sep 17 00:00:00 2001 From: karmeev Date: Sun, 10 Aug 2025 13:13:20 +0300 Subject: [PATCH 4/7] fix(WorldMapService): fix movement calculation --- .../src/Nasa.Pathfinder.Services/Internal/WorldMapService.cs | 2 +- .../Nasa.Pathfinder.Services.Tests/WorldMapServiceTests.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/nasa-server/src/Nasa.Pathfinder.Services/Internal/WorldMapService.cs b/src/nasa-server/src/Nasa.Pathfinder.Services/Internal/WorldMapService.cs index 84d3b65..1f67f13 100644 --- a/src/nasa-server/src/Nasa.Pathfinder.Services/Internal/WorldMapService.cs +++ b/src/nasa-server/src/Nasa.Pathfinder.Services/Internal/WorldMapService.cs @@ -74,7 +74,7 @@ public async Task TryReachPosition(string mapId, Position posi var isOutOfMap = false; var mapInfo = await mapRepository.TryGetAsync(mapId, ct); - if (position.X > mapInfo.SizeX || position.Y > mapInfo.SizeY) + if (position.X >= mapInfo.SizeX || position.Y >= mapInfo.SizeY) { isOutOfMap = true; } diff --git a/src/nasa-server/tests/Nasa.Pathfinder.Services.Tests/WorldMapServiceTests.cs b/src/nasa-server/tests/Nasa.Pathfinder.Services.Tests/WorldMapServiceTests.cs index 384304b..12a02f7 100644 --- a/src/nasa-server/tests/Nasa.Pathfinder.Services.Tests/WorldMapServiceTests.cs +++ b/src/nasa-server/tests/Nasa.Pathfinder.Services.Tests/WorldMapServiceTests.cs @@ -109,7 +109,7 @@ public async Task TryReachPosition_WhenFuneralWithDesiredPositionFindAndNotOutOf var desiredPosition = new Position { X = 1, - Y = 3, + Y = 2, Direction = Direction.W }; From 360a2e5f59b64e7347a582fe64affcc67f38cf47 Mon Sep 17 00:00:00 2001 From: karmeev Date: Sun, 10 Aug 2025 13:31:48 +0300 Subject: [PATCH 5/7] fix(WorldMapService): fix out of map --- .../Internal/WorldMapService.cs | 2 +- .../WorldMapServiceTests.cs | 30 +++++++++++++++---- 2 files changed, 26 insertions(+), 6 deletions(-) diff --git a/src/nasa-server/src/Nasa.Pathfinder.Services/Internal/WorldMapService.cs b/src/nasa-server/src/Nasa.Pathfinder.Services/Internal/WorldMapService.cs index 1f67f13..3153023 100644 --- a/src/nasa-server/src/Nasa.Pathfinder.Services/Internal/WorldMapService.cs +++ b/src/nasa-server/src/Nasa.Pathfinder.Services/Internal/WorldMapService.cs @@ -79,7 +79,7 @@ public async Task TryReachPosition(string mapId, Position posi isOutOfMap = true; } - if (isTrap && !isOutOfMap) + if (isTrap) { return PositionProject.NotChanged(); } diff --git a/src/nasa-server/tests/Nasa.Pathfinder.Services.Tests/WorldMapServiceTests.cs b/src/nasa-server/tests/Nasa.Pathfinder.Services.Tests/WorldMapServiceTests.cs index 12a02f7..923e524 100644 --- a/src/nasa-server/tests/Nasa.Pathfinder.Services.Tests/WorldMapServiceTests.cs +++ b/src/nasa-server/tests/Nasa.Pathfinder.Services.Tests/WorldMapServiceTests.cs @@ -75,15 +75,35 @@ public void CalculateDesiredPosition_WhenDesiredPositionOutOfGrid_ShouldReturnsD Direction = Direction.W }; + _mockFuneralRepository.Setup(x => x.GetFuneralsAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync([ + new Funeral + { + Id = "1", + ETag = Guid.NewGuid(), + MapId = "s", + Value = new Position + { + X = 2, + Y = 4, + Direction = Direction.E + } + } + ]); + var commands = new Stack(); commands.Push(new MoveLeft()); + commands.Push(new MoveFront()); commands.Push(new MoveLeft()); + commands.Push(new MoveFront()); commands.Push(new MoveLeft()); + commands.Push(new MoveFront()); commands.Push(new MoveFront()); commands.Push(new MoveFront()); + commands.Push(new MoveLeft()); commands.Push(new MoveLeft()); @@ -108,9 +128,9 @@ public async Task TryReachPosition_WhenFuneralWithDesiredPositionFindAndNotOutOf var desiredPosition = new Position { - X = 1, - Y = 2, - Direction = Direction.W + X = 2, + Y = 4, + Direction = Direction.S }; await TestRunner @@ -121,8 +141,8 @@ await TestRunner { Id = mapId, ETag = new Faker().Random.Guid(), - SizeX = 50, - SizeY = 50, + SizeX = 5, + SizeY = 3, }); _mockFuneralRepository.Setup(x => From 5c0a43220c8f38cea926e727c8367a3ad8dc71ff Mon Sep 17 00:00:00 2001 From: karmeev Date: Sun, 10 Aug 2025 17:09:47 +0300 Subject: [PATCH 6/7] fix(api): fix bot behaviour --- .../Internal/BotConsumer.cs | 13 +- .../Internal/BotDeadWalkerConsumer.cs | 6 +- .../Internal/BotWalkerConsumer.cs | 4 +- .../Interactions/BotCommands.cs | 7 +- .../World/PositionProject.cs | 22 +- .../Internal/MessageFacade.cs | 7 +- .../IWorldMapService.cs | 5 +- .../Internal/WorldMapService.cs | 163 ++++-- .../MessageFacadeTests.cs | 167 +----- .../WorldMapServiceTests.cs | 526 +++++++++++++----- 10 files changed, 540 insertions(+), 380 deletions(-) diff --git a/src/nasa-server/src/Nasa.Pathfinder.Consumers/Internal/BotConsumer.cs b/src/nasa-server/src/Nasa.Pathfinder.Consumers/Internal/BotConsumer.cs index b4bff92..5a2e715 100644 --- a/src/nasa-server/src/Nasa.Pathfinder.Consumers/Internal/BotConsumer.cs +++ b/src/nasa-server/src/Nasa.Pathfinder.Consumers/Internal/BotConsumer.cs @@ -12,24 +12,25 @@ internal class BotConsumer( { public async Task Consume(MoveCommand command, CancellationToken ct = default) { - var project = await worldMap.TryReachPosition(command.Bot.MapId, command.DesiredPosition, ct); + var project = await worldMap.ReachPosition(command.Bot.MapId, command.Bot.Position, command.OperatorCommands , ct); if (project is PositionNotChanged) { - var stand = new StandCommand(command.ClientId, command.Bot.Id, command.Bot.Position, command.CorrelationId); + var stand = new StandCommand(command.ClientId, command.Bot.Id, project.Position, + command.CorrelationId); processor.Publish(stand); return; } - if (project is PositionOutOfMap) + if (project is PositionOutOfMap outOfMap) { - var lost = new DeadCommand(command.ClientId, command.Bot.Id, command.DesiredPosition, command.Bot.MapId, - command.Bot.LastWords, command.CorrelationId); + var lost = new DeadCommand(command.ClientId, command.Bot.Id, command.Bot.MapId, + command.Bot.LastWords, outOfMap.Previous, command.CorrelationId); processor.Publish(lost); return; } - var walk = new WalkCommand(command.ClientId, command.Bot.Id, command.DesiredPosition, + var walk = new WalkCommand(command.ClientId, command.Bot.Id, project.Position, command.CorrelationId); processor.Publish(walk); } diff --git a/src/nasa-server/src/Nasa.Pathfinder.Consumers/Internal/BotDeadWalkerConsumer.cs b/src/nasa-server/src/Nasa.Pathfinder.Consumers/Internal/BotDeadWalkerConsumer.cs index 65dca39..1e7487e 100644 --- a/src/nasa-server/src/Nasa.Pathfinder.Consumers/Internal/BotDeadWalkerConsumer.cs +++ b/src/nasa-server/src/Nasa.Pathfinder.Consumers/Internal/BotDeadWalkerConsumer.cs @@ -18,17 +18,17 @@ internal class BotDeadWalkerConsumer( public async Task Consume(DeadCommand command, CancellationToken ct = default) { await botRepository.ChangeBotStatusAsync(command.BotId, BotStatus.Dead, ct); - await botRepository.ChangeBotPositionAsync(command.BotId, command.DesiredPosition, ct); + await botRepository.ChangeBotPositionAsync(command.BotId, command.CurrentPosition, ct); var funeral = new Funeral { Id = Guid.CreateVersion7().ToString(), ETag = Guid.NewGuid(), - Value = command.DesiredPosition, + Value = command.CurrentPosition, MapId = command.MapId, }; await funeralRepository.AddNewFuneral(funeral, ct); - var notificationText = messageDecoder.EncodeBotMessage(command.DesiredPosition, true); + var notificationText = messageDecoder.EncodeBotMessage(command.CurrentPosition, true); var request = new SendMessageRequest(command.ClientId, command.BotId, notificationText, true, false, command.LastWords); processor.SendMessage(request); diff --git a/src/nasa-server/src/Nasa.Pathfinder.Consumers/Internal/BotWalkerConsumer.cs b/src/nasa-server/src/Nasa.Pathfinder.Consumers/Internal/BotWalkerConsumer.cs index d21668c..f535a4e 100644 --- a/src/nasa-server/src/Nasa.Pathfinder.Consumers/Internal/BotWalkerConsumer.cs +++ b/src/nasa-server/src/Nasa.Pathfinder.Consumers/Internal/BotWalkerConsumer.cs @@ -14,8 +14,8 @@ internal class BotWalkerConsumer( { public async Task Consume(WalkCommand command, CancellationToken ct = default) { - await repository.ChangeBotPositionAsync(command.BotId, command.DesiredPosition, ct); - var notificationText = messageDecoder.EncodeBotMessage(command.DesiredPosition, false); + await repository.ChangeBotPositionAsync(command.BotId, command.CurrentPosition, ct); + var notificationText = messageDecoder.EncodeBotMessage(command.CurrentPosition, false); var request = new SendMessageRequest(command.ClientId, command.BotId, notificationText, false, false, string.Empty); processor.SendMessage(request); diff --git a/src/nasa-server/src/Nasa.Pathfinder.Domain/Interactions/BotCommands.cs b/src/nasa-server/src/Nasa.Pathfinder.Domain/Interactions/BotCommands.cs index e6d0827..e7f1712 100644 --- a/src/nasa-server/src/Nasa.Pathfinder.Domain/Interactions/BotCommands.cs +++ b/src/nasa-server/src/Nasa.Pathfinder.Domain/Interactions/BotCommands.cs @@ -1,4 +1,3 @@ -using Nasa.Pathfinder.Domain.Bots; using Nasa.Pathfinder.Domain.Entities.Bots; using Nasa.Pathfinder.Domain.World; @@ -12,13 +11,13 @@ public interface IBotCommand public record MoveCommand( string ClientId, Bot Bot, - Position DesiredPosition, + Stack OperatorCommands, Guid CorrelationId = default) : IBotCommand; public record WalkCommand( string ClientId, string BotId, - Position DesiredPosition, + Position CurrentPosition, Guid CorrelationId = default) : IBotCommand; public record StandCommand( @@ -30,9 +29,9 @@ public record StandCommand( public record DeadCommand( string ClientId, string BotId, - Position DesiredPosition, string MapId, string LastWords, + Position CurrentPosition, Guid CorrelationId = default) : IBotCommand; public record InvalidCommand( diff --git a/src/nasa-server/src/Nasa.Pathfinder.Domain/World/PositionProject.cs b/src/nasa-server/src/Nasa.Pathfinder.Domain/World/PositionProject.cs index 24d35ff..37c4e18 100644 --- a/src/nasa-server/src/Nasa.Pathfinder.Domain/World/PositionProject.cs +++ b/src/nasa-server/src/Nasa.Pathfinder.Domain/World/PositionProject.cs @@ -1,26 +1,28 @@ namespace Nasa.Pathfinder.Domain.World; public interface IPositionProject -{ } +{ + public Position Position { get; } +} -public record PositionChanged : IPositionProject; -public record PositionNotChanged : IPositionProject; -public record PositionOutOfMap : IPositionProject; +public record PositionChanged(Position Position) : IPositionProject; +public record PositionNotChanged(Position Position) : IPositionProject; +public record PositionOutOfMap(Position Position, Position Previous) : IPositionProject; public static class PositionProject { - public static IPositionProject Changed() + public static IPositionProject Changed(Position position) { - return new PositionChanged(); + return new PositionChanged(position); } - public static IPositionProject NotChanged() + public static IPositionProject NotChanged(Position position) { - return new PositionNotChanged(); + return new PositionNotChanged(position); } - public static IPositionProject OutOfMap() + public static IPositionProject OutOfMap(Position position, Position previous) { - return new PositionOutOfMap(); + return new PositionOutOfMap(position, previous); } } \ No newline at end of file diff --git a/src/nasa-server/src/Nasa.Pathfinder.Facades/Internal/MessageFacade.cs b/src/nasa-server/src/Nasa.Pathfinder.Facades/Internal/MessageFacade.cs index aff7774..d0b2f28 100644 --- a/src/nasa-server/src/Nasa.Pathfinder.Facades/Internal/MessageFacade.cs +++ b/src/nasa-server/src/Nasa.Pathfinder.Facades/Internal/MessageFacade.cs @@ -11,7 +11,6 @@ namespace Nasa.Pathfinder.Facades.Internal; internal class MessageFacade( IBotRepository repository, IMessageDecoderService messageDecoder, - IWorldMapService worldMap, IBotProcessor processor) : IMessageFacade { public async Task ReceiveMessageAsync(IMessage message, CancellationToken ct = default) @@ -37,9 +36,7 @@ public async Task ReceiveMessageAsync(IMessage message, CancellationToken ct = d } var bot = await repository.TryGetAsync(operatorMessage.BotId, ct); - - var desiredPosition = worldMap.CalculateDesiredPosition(bot.Position, commands); - - processor.Publish(new MoveCommand(operatorMessage.ClientId, bot, desiredPosition, correlationId)); + + processor.Publish(new MoveCommand(operatorMessage.ClientId, bot, commands, correlationId)); } } \ No newline at end of file diff --git a/src/nasa-server/src/Nasa.Pathfinder.Services.Contracts/IWorldMapService.cs b/src/nasa-server/src/Nasa.Pathfinder.Services.Contracts/IWorldMapService.cs index b383288..58e767c 100644 --- a/src/nasa-server/src/Nasa.Pathfinder.Services.Contracts/IWorldMapService.cs +++ b/src/nasa-server/src/Nasa.Pathfinder.Services.Contracts/IWorldMapService.cs @@ -5,7 +5,6 @@ namespace Nasa.Pathfinder.Services.Contracts; public interface IWorldMapService { - Position CalculateDesiredPosition(Position currentPosition, Stack commands); - Task TryReachPosition(string mapId, Position position, - CancellationToken ct = default); + Task ReachPosition(string mapId, Position currentPosition, + Stack commands, CancellationToken ct = default); } \ No newline at end of file diff --git a/src/nasa-server/src/Nasa.Pathfinder.Services/Internal/WorldMapService.cs b/src/nasa-server/src/Nasa.Pathfinder.Services/Internal/WorldMapService.cs index 3153023..301bfcd 100644 --- a/src/nasa-server/src/Nasa.Pathfinder.Services/Internal/WorldMapService.cs +++ b/src/nasa-server/src/Nasa.Pathfinder.Services/Internal/WorldMapService.cs @@ -10,85 +10,140 @@ internal class WorldMapService( IMapRepository mapRepository, IFuneralRepository funeralRepository) : IWorldMapService { - public Position CalculateDesiredPosition(Position currentPosition, Stack commands) + public async Task ReachPosition(string mapId, Position currentPosition, + Stack commands, CancellationToken ct = default) { - foreach (var command in commands) currentPosition = Move(currentPosition, command); - - return currentPosition; - - static Position Move(Position position, IOperatorCommand command) + var start = new Position { - if (command is MoveRight or MoveLeft) position.Direction = Rotate(command, position.Direction); + X = currentPosition.X, + Y = currentPosition.Y, + Direction = Direction.N + }; + var funerals = await funeralRepository.GetFuneralsAsync(mapId, ct); + + IPositionProject position = new PositionChanged(currentPosition); - switch (position.Direction) + foreach (var command in commands) + { + Position current; + switch (command) { - case Direction.N: - position.Y += command.Steps; - break; - case Direction.E: - position.X += command.Steps; + case MoveRight or MoveLeft: + current = position.Position; + + current.Direction = Rotate(command, current.Direction); + position = new PositionChanged(current); + + Console.WriteLine($"Coordinates: {position.Position.X} x {position.Position.Y}, " + + $"{position.Position.Direction}"); break; - case Direction.S: - position.Y -= command.Steps; - break; - case Direction.W: - position.X -= command.Steps; + case MoveFront moveFrontCommand: + current = position.Position; + + var newPosition = await CalculateMoveFront(current, moveFrontCommand, + funerals, mapId, ct); + + if (newPosition is PositionNotChanged) + { + Console.WriteLine($"Coordinates: {current.X} x {current.Y}, {current.Direction}"); + continue; + } + + if (newPosition is PositionOutOfMap @out) + { + Console.WriteLine($"Coordinates: {newPosition.Position.X} x {newPosition.Position.Y}, " + + $"{newPosition.Position.Direction}"); + return @out; + } + + position = newPosition; + Console.WriteLine($"Coordinates: {position.Position.X} x {position.Position.Y}, {position.Position.Direction}"); break; default: - throw new ArgumentOutOfRangeException(); + continue; } - - return position; } - static Direction Rotate(IOperatorCommand command, Direction direction) + if (position.Position.X == start.X && position.Position.Y == start.Y && + position.Position.Direction == start.Direction) { - return command switch - { - MoveLeft _ => direction switch - { - Direction.N => Direction.W, - Direction.W => Direction.S, - Direction.S => Direction.E, - Direction.E => Direction.N, - _ => throw new ArgumentOutOfRangeException() - }, - MoveRight _ => direction switch - { - Direction.N => Direction.E, - Direction.E => Direction.S, - Direction.S => Direction.W, - Direction.W => Direction.N, - _ => throw new ArgumentOutOfRangeException() - } - }; + return new PositionNotChanged(currentPosition); } + + return position; } - public async Task TryReachPosition(string mapId, Position position, - CancellationToken ct = default) + private async Task CalculateMoveFront(Position position, MoveFront command, + IReadOnlyCollection funerals, string mapId, CancellationToken ct = default) { - var funerals = await funeralRepository.GetFuneralsAsync(mapId, ct); - var isTrap = funerals.Any(x => x.Value == position); - - var isOutOfMap = false; - var mapInfo = await mapRepository.TryGetAsync(mapId, ct); + var previous = new Position + { + X = position.X, + Y = position.Y, + Direction = position.Direction + }; - if (position.X >= mapInfo.SizeX || position.Y >= mapInfo.SizeY) + var isTrap = funerals.Any(x => x.Value.X == position.X + && x.Value.Y == position.Y + && x.Value.Direction == position.Direction); + if (isTrap) { - isOutOfMap = true; + return PositionProject.NotChanged(position); } - if (isTrap) + switch (position.Direction) { - return PositionProject.NotChanged(); + case Direction.N: + position.Y += command.Steps; + break; + case Direction.E: + position.X += command.Steps; + break; + case Direction.S: + position.Y -= command.Steps; + break; + case Direction.W: + position.X -= command.Steps; + break; + default: + throw new ArgumentOutOfRangeException(); + } + + var isOutOfMap = false; + var mapInfo = await mapRepository.TryGetAsync(mapId, ct); + if (position.X > mapInfo.SizeX || position.Y > mapInfo.SizeY) + { + isOutOfMap = true; } if (isOutOfMap) { - return PositionProject.OutOfMap(); + return PositionProject.OutOfMap(position, previous); } - return PositionProject.Changed(); + return PositionProject.Changed(position); + } + + private static Direction Rotate(IOperatorCommand command, Direction direction) + { + return command switch + { + MoveLeft _ => direction switch + { + Direction.N => Direction.W, + Direction.W => Direction.S, + Direction.S => Direction.E, + Direction.E => Direction.N, + _ => throw new ArgumentOutOfRangeException() + }, + MoveRight _ => direction switch + { + Direction.N => Direction.E, + Direction.E => Direction.S, + Direction.S => Direction.W, + Direction.W => Direction.N, + _ => throw new ArgumentOutOfRangeException() + } + }; } } \ No newline at end of file diff --git a/src/nasa-server/tests/Nasa.Pathfinder.Facades.Tests/MessageFacadeTests.cs b/src/nasa-server/tests/Nasa.Pathfinder.Facades.Tests/MessageFacadeTests.cs index 1ac99bf..c17538d 100644 --- a/src/nasa-server/tests/Nasa.Pathfinder.Facades.Tests/MessageFacadeTests.cs +++ b/src/nasa-server/tests/Nasa.Pathfinder.Facades.Tests/MessageFacadeTests.cs @@ -34,8 +34,7 @@ public void Setup() public async Task ReceiveMessageAsync_UnsupportedMessage_ShouldThrowsException() { await TestRunner - .Arrange(() => new MessageFacade(_repositoryMock.Object, _decoderServiceMock.Object, - _worldMapServiceMock.Object, _processorServiceMock.Object)) + .Arrange(() => new MessageFacade(_repositoryMock.Object, _decoderServiceMock.Object, _processorServiceMock.Object)) .ActAsync(sut => sut.ReceiveMessageAsync(new BotMessage())) .ThenAssertThrowsAsync(); } @@ -75,17 +74,7 @@ await TestRunner } }); - _worldMapServiceMock.Setup(x => x.CalculateDesiredPosition(It.IsAny(), - It.IsAny>())) - .Returns(new Position - { - X = 4, - Y = 3, - Direction = Direction.N - }); - - return new MessageFacade(_repositoryMock.Object, _decoderServiceMock.Object, - _worldMapServiceMock.Object, _processorServiceMock.Object); + return new MessageFacade(_repositoryMock.Object, _decoderServiceMock.Object, _processorServiceMock.Object); }) .ActAsync(sut => sut.ReceiveMessageAsync(new OperatorMessage { @@ -100,156 +89,4 @@ await TestRunner Times.Once); }); } - - // [Test] - // public async Task ReceiveMessageAsync_HasFuneral_ShouldStayAtPlace() - // { - // const string input = "FRFFL"; - // var botId = new Faker().Random.Hash(); - // var clientId = new Faker().Random.Hash(); - // - // var desiredPosition = new Position - // { - // X = 4, - // Y = 3, - // Direction = Direction.N - // }; - // - // await TestRunner - // .Arrange(() => - // { - // _decoderServiceMock.Setup(x => x.DecodeOperatorMessage( - // It.Is(i => i.Equals(input)))) - // .Returns(new List - // { - // new MoveFront(), - // new MoveRight(), - // new MoveFront(), - // new MoveFront(), - // new MoveLeft() - // }); - // - // _repositoryMock.Setup(x => x.GetAsync(It.IsAny(), It.IsAny())) - // .ReturnsAsync(new Bot - // { - // Id = botId, - // Name = new Faker().Hacker.Noun(), - // Status = BotStatus.Acquired, - // Position = new Position - // { - // X = 1, - // Y = 1, - // Direction = Direction.N - // } - // }); - // - // _worldMapServiceMock.Setup(x => x.CalculateDesiredPosition(It.IsAny(), - // It.IsAny>())) - // .Returns(desiredPosition); - // - // _worldMapServiceMock.Setup(x => x.GetFuneralsAsync(It.IsAny())) - // .ReturnsAsync([desiredPosition]); - // - // _worldMapServiceMock.Setup(x => - // x.TryReachPosition(It.IsAny(), It.IsAny())) - // .ReturnsAsync(true); - // - // _decoderServiceMock.Setup(x => - // x.EncodeBotMessage( - // It.IsAny(), - // It.Is(b => b == false), - // It.Is(b => b == false))) - // .Returns("4 3 N"); - // - // return new MessageFacade(_repositoryMock.Object, _decoderServiceMock.Object, - // _worldMapServiceMock.Object); - // }) - // .ActAsync(sut => sut.ReceiveMessageAsync(new OperatorMessage - // { - // BotId = botId, - // ClientId = clientId, - // Text = input - // })) - // .ThenAssertAsync(() => - // { - // _worldMapServiceMock.Verify(x => - // x.ChangeBotPositionAsync(It.IsAny(), It.IsAny()), - // Times.Never); - // - // _streamMock.Verify(x => x.SendMessage(It.IsAny()), Times.Once); - // }); - // } - // - // [Test] - // public async Task ReceiveMessageAsync_CantReachPosition_ShouldLost() - // { - // const string input = "FRFFL"; - // var botId = new Faker().Random.Hash(); - // var clientId = new Faker().Random.Hash(); - // - // var desiredPosition = new Position - // { - // X = 199, - // Y = 3, - // Direction = Direction.N - // }; - // - // await TestRunner - // .Arrange(() => - // { - // _decoderServiceMock.Setup(x => x.DecodeOperatorMessage( - // It.Is(i => i.Equals(input)))) - // .Returns([]); - // - // _repositoryMock.Setup(x => x.GetAsync(It.IsAny(), It.IsAny())) - // .ReturnsAsync(new Bot - // { - // Id = botId, - // Name = new Faker().Hacker.Noun(), - // Status = BotStatus.Acquired, - // Position = new Position - // { - // X = 1, - // Y = 1, - // Direction = Direction.N - // } - // }); - // - // _worldMapServiceMock.Setup(x => x.CalculateDesiredPosition(It.IsAny(), - // It.IsAny>())) - // .Returns(desiredPosition); - // - // _worldMapServiceMock.Setup(x => x.GetFuneralsAsync(It.IsAny())) - // .ReturnsAsync([]); - // - // _worldMapServiceMock.Setup(x => - // x.TryReachPosition(It.IsAny(), It.IsAny())) - // .ReturnsAsync(false); - // - // _decoderServiceMock.Setup(x => - // x.EncodeBotMessage( - // It.IsAny(), - // It.Is(b => b == false), - // It.Is(b => b == false))) - // .Returns("199 3 N"); - // - // return new MessageFacade(_repositoryMock.Object, _decoderServiceMock.Object, - // _worldMapServiceMock.Object); - // }) - // .ActAsync(sut => sut.ReceiveMessageAsync(new OperatorMessage - // { - // BotId = botId, - // ClientId = clientId, - // Text = input - // })) - // .ThenAssertAsync(() => - // { - // _worldMapServiceMock.Verify(x => - // x.ChangeBotPositionAsync(It.IsAny(), It.IsAny()), - // Times.Once); - // - // _streamMock.Verify(x => - // x.SendMessage(It.Is(r => r.IsLost)), Times.Once); - // }); - // } } \ No newline at end of file diff --git a/src/nasa-server/tests/Nasa.Pathfinder.Services.Tests/WorldMapServiceTests.cs b/src/nasa-server/tests/Nasa.Pathfinder.Services.Tests/WorldMapServiceTests.cs index 923e524..86110eb 100644 --- a/src/nasa-server/tests/Nasa.Pathfinder.Services.Tests/WorldMapServiceTests.cs +++ b/src/nasa-server/tests/Nasa.Pathfinder.Services.Tests/WorldMapServiceTests.cs @@ -24,114 +24,34 @@ public void Setup() private Mock _mockMapRepository; private Mock _mockFuneralRepository; - [Test] - public void CalculateDesiredPosition_WhenWeGotListOfCommands_ShouldReturnsDestination() - { - TestRunner - .Arrange(() => new WorldMapService(_mockMapRepository.Object, _mockFuneralRepository.Object)) - .Act(sut => - { - var currentPosition = new Position - { - X = 1, - Y = 1, - Direction = Direction.E - }; - - var commands = new Stack(); - commands.Push(new MoveFront()); - commands.Push(new MoveRight()); - commands.Push(new MoveFront()); - commands.Push(new MoveRight()); - commands.Push(new MoveFront()); - commands.Push(new MoveRight()); - commands.Push(new MoveFront()); - commands.Push(new MoveRight()); - - return sut.CalculateDesiredPosition(currentPosition, commands); - }) - .Assert(position => - { - Assert.Multiple(() => - { - Assert.That(position.X, Is.EqualTo(1)); - Assert.That(position.Y, Is.EqualTo(1)); - Assert.That(position.Direction, Is.EqualTo(Direction.E)); - }); - }); - } [Test] - public void CalculateDesiredPosition_WhenDesiredPositionOutOfGrid_ShouldReturnsDestination() + public async Task ReachPosition_WhenFuneralWithDesiredPositionFindAndNotOutOfMap_ShouldReturnNotChanged() { - TestRunner - .Arrange(() => new WorldMapService(_mockMapRepository.Object, _mockFuneralRepository.Object)) - .Act(sut => - { - var currentPosition = new Position - { - X = 0, - Y = 3, - Direction = Direction.W - }; + var mapId = $"map_{new Faker().Database.Random.Uuid()}"; - _mockFuneralRepository.Setup(x => x.GetFuneralsAsync(It.IsAny(), It.IsAny())) - .ReturnsAsync([ - new Funeral - { - Id = "1", - ETag = Guid.NewGuid(), - MapId = "s", - Value = new Position - { - X = 2, - Y = 4, - Direction = Direction.E - } - } - ]); + var currentPosition = new Position + { + X = 0, + Y = 3, + Direction = Direction.W + }; + + var commands = new Stack(); + commands.Push(new MoveLeft()); - var commands = new Stack(); - commands.Push(new MoveLeft()); - - commands.Push(new MoveFront()); - commands.Push(new MoveLeft()); - - commands.Push(new MoveFront()); - commands.Push(new MoveLeft()); - - commands.Push(new MoveFront()); - commands.Push(new MoveFront()); - commands.Push(new MoveFront()); - - commands.Push(new MoveLeft()); - commands.Push(new MoveLeft()); + commands.Push(new MoveFront()); + commands.Push(new MoveLeft()); + commands.Push(new MoveFront()); + commands.Push(new MoveLeft()); - return sut.CalculateDesiredPosition(currentPosition, commands); - }) - .Assert(position => - { - Assert.Multiple(() => - { - Assert.That(position.X, Is.EqualTo(2)); - Assert.That(position.Y, Is.EqualTo(4)); - Assert.That(position.Direction, Is.EqualTo(Direction.S)); - }); - }); - } + commands.Push(new MoveFront()); + commands.Push(new MoveFront()); + commands.Push(new MoveFront()); - [Test] - public async Task TryReachPosition_WhenFuneralWithDesiredPositionFindAndNotOutOfMap_ShouldReturnNotChanged() - { - var mapId = $"map_{new Faker().Database.Random.Uuid()}"; - - var desiredPosition = new Position - { - X = 2, - Y = 4, - Direction = Direction.S - }; + commands.Push(new MoveLeft()); + commands.Push(new MoveLeft()); await TestRunner .Arrange(() => @@ -154,30 +74,59 @@ await TestRunner Id = new Faker().Random.Guid().ToString(), ETag = new Faker().Random.Guid(), MapId = mapId, - Value = desiredPosition + Value = new Position + { + X = 3, + Y = 3, + Direction = Direction.N + } } }); return new WorldMapService(_mockMapRepository.Object, _mockFuneralRepository.Object); }) - .ActAsync(async sut => await sut.TryReachPosition(mapId, desiredPosition)) - .ThenAssertAsync(position => + .ActAsync(async sut => await sut.ReachPosition(mapId, currentPosition, commands)) + .ThenAssertAsync(positionProject => { - Assert.That(position, Is.TypeOf()); + var desiredPosition = new Position + { + X = 2, + Y = 3, + Direction = Direction.S + }; + + Assert.Multiple(() => + { + Assert.That(positionProject, Is.TypeOf()); + Assert.That(positionProject.Position.X, Is.EqualTo(desiredPosition.X)); + Assert.That(positionProject.Position.Y, Is.EqualTo(desiredPosition.Y)); + Assert.That(positionProject.Position.Direction, Is.EqualTo(desiredPosition.Direction)); + }); + }); } [Test] - public async Task TryReachPosition_WhenFuneralWithDesiredPositionOutOfMap_ShouldReturnNotChanged() + public async Task ReachPosition_WhenBotCanMove_ShouldReturnChanged() { var mapId = $"map_{new Faker().Database.Random.Uuid()}"; - - var desiredPosition = new Position + + var currentPosition = new Position { - X = 51, - Y = 3, - Direction = Direction.W + X = 1, + Y = 1, + Direction = Direction.E }; + + var commands = new Stack(); + commands.Push(new MoveFront()); + commands.Push(new MoveRight()); + commands.Push(new MoveFront()); + commands.Push(new MoveRight()); + commands.Push(new MoveFront()); + commands.Push(new MoveRight()); + commands.Push(new MoveFront()); + commands.Push(new MoveRight()); await TestRunner .Arrange(() => @@ -187,34 +136,63 @@ await TestRunner { Id = mapId, ETag = new Faker().Random.Guid(), - SizeX = 50, - SizeY = 50, + SizeX = 5, + SizeY = 3, }); _mockFuneralRepository.Setup(x => x.GetFuneralsAsync(mapId, It.IsAny())) - .ReturnsAsync(new List { FakeFuneral.Make(mapId) }); + .ReturnsAsync(new List()); return new WorldMapService(_mockMapRepository.Object, _mockFuneralRepository.Object); }) - .ActAsync(async sut => await sut.TryReachPosition(mapId, desiredPosition)) - .ThenAssertAsync(position => + .ActAsync(async sut => await sut.ReachPosition(mapId, currentPosition, commands)) + .ThenAssertAsync(positionProject => { - Assert.That(position, Is.TypeOf()); + var desiredPosition = new Position + { + X = currentPosition.X, + Y = currentPosition.Y, + Direction = currentPosition.Direction + }; + + Assert.Multiple(() => + { + Assert.That(positionProject, Is.TypeOf()); + Assert.That(positionProject.Position.X, Is.EqualTo(desiredPosition.X)); + Assert.That(positionProject.Position.Y, Is.EqualTo(desiredPosition.Y)); + Assert.That(positionProject.Position.Direction, Is.EqualTo(desiredPosition.Direction)); + }); + }); } [Test] - public async Task TryReachPosition_WhenBotCanMove_ShouldReturnChanged() + public async Task ReachPosition_WhenDesiredPositionOutOfGrid_ShouldReturnsDestination() { var mapId = $"map_{new Faker().Database.Random.Uuid()}"; - - var desiredPosition = new Position + + var currentPosition = new Position { - X = 4, - Y = 3, - Direction = Direction.W + X = 3, + Y = 2, + Direction = Direction.N }; + + var commands = new Stack(); + commands.Push(new MoveLeft()); + commands.Push(new MoveLeft()); + commands.Push(new MoveFront()); + commands.Push(new MoveRight()); + commands.Push(new MoveRight()); + commands.Push(new MoveFront()); + commands.Push(new MoveFront()); + commands.Push(new MoveLeft()); + commands.Push(new MoveLeft()); + commands.Push(new MoveFront()); + commands.Push(new MoveRight()); + commands.Push(new MoveRight()); + commands.Push(new MoveFront()); await TestRunner .Arrange(() => @@ -224,20 +202,312 @@ await TestRunner { Id = mapId, ETag = new Faker().Random.Guid(), - SizeX = 50, - SizeY = 50, + SizeX = 5, + SizeY = 3, }); _mockFuneralRepository.Setup(x => x.GetFuneralsAsync(mapId, It.IsAny())) - .ReturnsAsync(new List { FakeFuneral.Make(mapId) }); + .ReturnsAsync(new List()); return new WorldMapService(_mockMapRepository.Object, _mockFuneralRepository.Object); }) - .ActAsync(async sut => await sut.TryReachPosition(mapId, desiredPosition)) - .ThenAssertAsync(position => + .ActAsync(async sut => await sut.ReachPosition(mapId, currentPosition, commands)) + .ThenAssertAsync(positionProject => { - Assert.That(position, Is.TypeOf()); + Assert.Multiple(() => + { + Assert.That(positionProject, Is.TypeOf()); + + Assert.That(positionProject.Position.X, Is.EqualTo(3)); + Assert.That(positionProject.Position.Y, Is.EqualTo(4)); + Assert.That(positionProject.Position.Direction, Is.EqualTo(Direction.N)); + + var @out = positionProject as PositionOutOfMap; + Assert.That(@out.Previous.X, Is.EqualTo(3)); + Assert.That(@out.Previous.Y, Is.EqualTo(3)); + Assert.That(@out.Previous.Direction, Is.EqualTo(Direction.N)); + }); + }); } + + + // + // [Test] + // public async Task ReachPosition_WhenFuneralWithDesiredPositionOutOfMap_ShouldReturnNotChanged() + // { + // var mapId = $"map_{new Faker().Database.Random.Uuid()}"; + // + // var desiredPosition = new Position + // { + // X = 51, + // Y = 3, + // Direction = Direction.W + // }; + // + // await TestRunner + // .Arrange(() => + // { + // _mockMapRepository.Setup(x => x.TryGetAsync(mapId, It.IsAny())) + // .ReturnsAsync(new MapInfo + // { + // Id = mapId, + // ETag = new Faker().Random.Guid(), + // SizeX = 50, + // SizeY = 50, + // }); + // + // _mockFuneralRepository.Setup(x => + // x.GetFuneralsAsync(mapId, It.IsAny())) + // .ReturnsAsync(new List { FakeFuneral.Make(mapId) }); + // + // return new WorldMapService(_mockMapRepository.Object, _mockFuneralRepository.Object); + // }) + // .ActAsync(async sut => await sut.TryReachPosition(mapId, desiredPosition)) + // .ThenAssertAsync(position => { Assert.That(position, Is.TypeOf()); }); + // } + // + // [Test] + // public async Task ReachPosition_WhenBotCanMove_ShouldReturnChanged() + // { + // var mapId = $"map_{new Faker().Database.Random.Uuid()}"; + // + // var desiredPosition = new Position + // { + // X = 4, + // Y = 3, + // Direction = Direction.W + // }; + // + // await TestRunner + // .Arrange(() => + // { + // _mockMapRepository.Setup(x => x.TryGetAsync(mapId, It.IsAny())) + // .ReturnsAsync(new MapInfo + // { + // Id = mapId, + // ETag = new Faker().Random.Guid(), + // SizeX = 50, + // SizeY = 50, + // }); + // + // _mockFuneralRepository.Setup(x => + // x.GetFuneralsAsync(mapId, It.IsAny())) + // .ReturnsAsync(new List { FakeFuneral.Make(mapId) }); + // + // return new WorldMapService(_mockMapRepository.Object, _mockFuneralRepository.Object); + // }) + // .ActAsync(async sut => await sut.TryReachPosition(mapId, desiredPosition)) + // .ThenAssertAsync(position => { Assert.That(position, Is.TypeOf()); }); + // } + // + // + // [Test] + // public void CalculateDesiredPosition_WhenWeGotListOfCommands_ShouldReturnsDestination() + // { + // TestRunner + // .Arrange(() => new WorldMapService(_mockMapRepository.Object, _mockFuneralRepository.Object)) + // .Act(sut => + // { + // var currentPosition = new Position + // { + // X = 1, + // Y = 1, + // Direction = Direction.E + // }; + // + // var commands = new Stack(); + // commands.Push(new MoveFront()); + // commands.Push(new MoveRight()); + // commands.Push(new MoveFront()); + // commands.Push(new MoveRight()); + // commands.Push(new MoveFront()); + // commands.Push(new MoveRight()); + // commands.Push(new MoveFront()); + // commands.Push(new MoveRight()); + // + // return sut.CalculateDesiredPosition(currentPosition, commands); + // }) + // .Assert(position => + // { + // Assert.Multiple(() => + // { + // Assert.That(position.X, Is.EqualTo(1)); + // Assert.That(position.Y, Is.EqualTo(1)); + // Assert.That(position.Direction, Is.EqualTo(Direction.E)); + // }); + // }); + // } + // + // [Test] + // public void CalculateDesiredPosition_WhenDesiredPositionOutOfGrid_ShouldReturnsDestination() + // { + // TestRunner + // .Arrange(() => new WorldMapService(_mockMapRepository.Object, _mockFuneralRepository.Object)) + // .Act(sut => + // { + // var currentPosition = new Position + // { + // X = 0, + // Y = 3, + // Direction = Direction.W + // }; + // + // _mockFuneralRepository.Setup(x => x.GetFuneralsAsync(It.IsAny(), It.IsAny())) + // .ReturnsAsync([ + // new Funeral + // { + // Id = "1", + // ETag = Guid.NewGuid(), + // MapId = "s", + // Value = new Position + // { + // X = 2, + // Y = 4, + // Direction = Direction.E + // } + // } + // ]); + // + // var commands = new Stack(); + // commands.Push(new MoveLeft()); + // + // commands.Push(new MoveFront()); + // commands.Push(new MoveLeft()); + // + // commands.Push(new MoveFront()); + // commands.Push(new MoveLeft()); + // + // commands.Push(new MoveFront()); + // commands.Push(new MoveFront()); + // commands.Push(new MoveFront()); + // + // commands.Push(new MoveLeft()); + // commands.Push(new MoveLeft()); + // + // + // return sut.CalculateDesiredPosition(currentPosition, commands); + // }) + // .Assert(position => + // { + // Assert.Multiple(() => + // { + // Assert.That(position.X, Is.EqualTo(2)); + // Assert.That(position.Y, Is.EqualTo(4)); + // Assert.That(position.Direction, Is.EqualTo(Direction.S)); + // }); + // }); + // } + // + // [Test] + // public async Task TryReachPosition_WhenFuneralWithDesiredPositionFindAndNotOutOfMap_ShouldReturnNotChanged() + // { + // var mapId = $"map_{new Faker().Database.Random.Uuid()}"; + // + // var desiredPosition = new Position + // { + // X = 2, + // Y = 4, + // Direction = Direction.S + // }; + // + // await TestRunner + // .Arrange(() => + // { + // _mockMapRepository.Setup(x => x.TryGetAsync(mapId, It.IsAny())) + // .ReturnsAsync(new MapInfo + // { + // Id = mapId, + // ETag = new Faker().Random.Guid(), + // SizeX = 5, + // SizeY = 3, + // }); + // + // _mockFuneralRepository.Setup(x => + // x.GetFuneralsAsync(mapId, It.IsAny())) + // .ReturnsAsync(new List + // { + // new() + // { + // Id = new Faker().Random.Guid().ToString(), + // ETag = new Faker().Random.Guid(), + // MapId = mapId, + // Value = desiredPosition + // } + // }); + // + // return new WorldMapService(_mockMapRepository.Object, _mockFuneralRepository.Object); + // }) + // .ActAsync(async sut => await sut.TryReachPosition(mapId, desiredPosition)) + // .ThenAssertAsync(position => { Assert.That(position, Is.TypeOf()); }); + // } + // + // [Test] + // public async Task TryReachPosition_WhenFuneralWithDesiredPositionOutOfMap_ShouldReturnNotChanged() + // { + // var mapId = $"map_{new Faker().Database.Random.Uuid()}"; + // + // var desiredPosition = new Position + // { + // X = 51, + // Y = 3, + // Direction = Direction.W + // }; + // + // await TestRunner + // .Arrange(() => + // { + // _mockMapRepository.Setup(x => x.TryGetAsync(mapId, It.IsAny())) + // .ReturnsAsync(new MapInfo + // { + // Id = mapId, + // ETag = new Faker().Random.Guid(), + // SizeX = 50, + // SizeY = 50, + // }); + // + // _mockFuneralRepository.Setup(x => + // x.GetFuneralsAsync(mapId, It.IsAny())) + // .ReturnsAsync(new List { FakeFuneral.Make(mapId) }); + // + // return new WorldMapService(_mockMapRepository.Object, _mockFuneralRepository.Object); + // }) + // .ActAsync(async sut => await sut.TryReachPosition(mapId, desiredPosition)) + // .ThenAssertAsync(position => { Assert.That(position, Is.TypeOf()); }); + // } + // + // [Test] + // public async Task TryReachPosition_WhenBotCanMove_ShouldReturnChanged() + // { + // var mapId = $"map_{new Faker().Database.Random.Uuid()}"; + // + // var desiredPosition = new Position + // { + // X = 4, + // Y = 3, + // Direction = Direction.W + // }; + // + // await TestRunner + // .Arrange(() => + // { + // _mockMapRepository.Setup(x => x.TryGetAsync(mapId, It.IsAny())) + // .ReturnsAsync(new MapInfo + // { + // Id = mapId, + // ETag = new Faker().Random.Guid(), + // SizeX = 50, + // SizeY = 50, + // }); + // + // _mockFuneralRepository.Setup(x => + // x.GetFuneralsAsync(mapId, It.IsAny())) + // .ReturnsAsync(new List { FakeFuneral.Make(mapId) }); + // + // return new WorldMapService(_mockMapRepository.Object, _mockFuneralRepository.Object); + // }) + // .ActAsync(async sut => await sut.TryReachPosition(mapId, desiredPosition)) + // .ThenAssertAsync(position => { Assert.That(position, Is.TypeOf()); }); + // } } \ No newline at end of file From 20df756d7a9cf6da24fe0c85ba5e844a2598ed02 Mon Sep 17 00:00:00 2001 From: karmeev Date: Sun, 10 Aug 2025 18:54:51 +0300 Subject: [PATCH 7/7] fix(api): fix calculation logic and extend api 1. fix logic with calculation. change behavior on traps. 2. add new endpoint for world creation --- api/messages.proto | 17 +- api/pathfinder.proto | 3 +- .../Internal/Pathfinder/PathfinderClient.cs | 2 +- .../Internal/BotConsumer.cs | 2 +- .../Internal/BotDeadWalkerConsumer.cs | 2 +- .../Internal/BotInvalidCommandConsumer.cs | 2 +- .../Internal/BotStandConsumer.cs | 2 +- .../Internal/BotWalkerConsumer.cs | 2 +- .../Repositories/IBotRepository.cs | 3 +- .../Repositories/IMapRepository.cs | 1 + .../Internal/BotRepository.cs | 22 +- .../Internal/MapRepository.cs | 9 + .../Entities/Bots/Bot.cs | 1 - .../Interactions/BotCommands.cs | 1 - .../IBotFacade.cs | 5 +- .../Internal/BotFacade.cs | 29 +- .../DataContexts/IDataContext.cs | 1 + .../Grpc/Requests/SendMessageRequest.cs | 3 +- .../Internal/Memory/MemoryDataContext.cs | 15 + .../Internal/WorldMapService.cs | 89 +++--- .../Background/MigrationBackgroundTask.cs | 24 +- .../src/Nasa.Pathfinder/Hubs/MessageHub.cs | 3 +- .../Services/PathfinderGrpcService.cs | 13 +- .../src/Nasa.Pathfinder/Types/Request.cs | 28 ++ .../src/Nasa.Pathfinder/Types/Response.cs | 8 + .../BotFacadeTests.cs | 29 +- .../WorldMapServiceTests.cs | 283 +----------------- .../PathfinderServiceTests.cs | 259 ++++++++-------- .../Nasa.IntegrationTests/TestRunner.cs | 15 +- 29 files changed, 356 insertions(+), 517 deletions(-) create mode 100644 src/nasa-server/src/Nasa.Pathfinder/Types/Request.cs diff --git a/api/messages.proto b/api/messages.proto index b102835..e54e377 100644 --- a/api/messages.proto +++ b/api/messages.proto @@ -6,8 +6,23 @@ message PingResponse { bool IsSuccessful = 1; } +message CreateWorldRequest { + int32 SizeX = 1; + int32 SizeY = 2; + repeated Bot Bots = 3; +} + +message CreateWorldResponse { + string MapId = 1; +} + +message GetBotsRequest { + string MapId = 1; +} + message GetBotsResponse { - repeated Bot Bots = 1; + string MapId = 1; + repeated Bot Bots = 2; } message SelectBotRequest { diff --git a/api/pathfinder.proto b/api/pathfinder.proto index 1e41950..098639e 100644 --- a/api/pathfinder.proto +++ b/api/pathfinder.proto @@ -8,8 +8,9 @@ import "messages.proto"; import "google/protobuf/empty.proto"; service PathfinderService { + rpc CreateWorld(pathfinder.messages.CreateWorldRequest) returns(pathfinder.messages.CreateWorldResponse); rpc Ping(google.protobuf.Empty) returns(pathfinder.messages.PingResponse); - rpc GetBots(google.protobuf.Empty) returns(pathfinder.messages.GetBotsResponse); + rpc GetBots(pathfinder.messages.GetBotsRequest) returns(pathfinder.messages.GetBotsResponse); rpc SelectBot(pathfinder.messages.SelectBotRequest) returns(pathfinder.messages.SelectBotResponse); rpc ResetBot(pathfinder.messages.ResetBotRequest) returns(pathfinder.messages.ResetBotResponse); rpc SendMessage(stream pathfinder.messages.SendMessageRequest) returns (stream pathfinder.messages.SendMessageResponse); diff --git a/src/dashboard-tui/Nasa.Dashboard.Clients/Internal/Pathfinder/PathfinderClient.cs b/src/dashboard-tui/Nasa.Dashboard.Clients/Internal/Pathfinder/PathfinderClient.cs index 0494fd0..7b0ee8e 100644 --- a/src/dashboard-tui/Nasa.Dashboard.Clients/Internal/Pathfinder/PathfinderClient.cs +++ b/src/dashboard-tui/Nasa.Dashboard.Clients/Internal/Pathfinder/PathfinderClient.cs @@ -24,7 +24,7 @@ public async Task PingAsync() public async Task> GetBotsAsync() { - var response = await grpcService.GetBotsAsync(new Empty(), GetHeader()); + var response = await grpcService.GetBotsAsync(new GetBotsRequest{MapId = "1"}, GetHeader()); return response.Bots.Select(bot => new Bot { diff --git a/src/nasa-server/src/Nasa.Pathfinder.Consumers/Internal/BotConsumer.cs b/src/nasa-server/src/Nasa.Pathfinder.Consumers/Internal/BotConsumer.cs index 5a2e715..a375b8f 100644 --- a/src/nasa-server/src/Nasa.Pathfinder.Consumers/Internal/BotConsumer.cs +++ b/src/nasa-server/src/Nasa.Pathfinder.Consumers/Internal/BotConsumer.cs @@ -25,7 +25,7 @@ public async Task Consume(MoveCommand command, CancellationToken ct = default) if (project is PositionOutOfMap outOfMap) { var lost = new DeadCommand(command.ClientId, command.Bot.Id, command.Bot.MapId, - command.Bot.LastWords, outOfMap.Previous, command.CorrelationId); + outOfMap.Previous, command.CorrelationId); processor.Publish(lost); return; } diff --git a/src/nasa-server/src/Nasa.Pathfinder.Consumers/Internal/BotDeadWalkerConsumer.cs b/src/nasa-server/src/Nasa.Pathfinder.Consumers/Internal/BotDeadWalkerConsumer.cs index 1e7487e..68ddec1 100644 --- a/src/nasa-server/src/Nasa.Pathfinder.Consumers/Internal/BotDeadWalkerConsumer.cs +++ b/src/nasa-server/src/Nasa.Pathfinder.Consumers/Internal/BotDeadWalkerConsumer.cs @@ -30,7 +30,7 @@ public async Task Consume(DeadCommand command, CancellationToken ct = default) await funeralRepository.AddNewFuneral(funeral, ct); var notificationText = messageDecoder.EncodeBotMessage(command.CurrentPosition, true); var request = new SendMessageRequest(command.ClientId, command.BotId, notificationText, true, - false, command.LastWords); + false); processor.SendMessage(request); } } \ No newline at end of file diff --git a/src/nasa-server/src/Nasa.Pathfinder.Consumers/Internal/BotInvalidCommandConsumer.cs b/src/nasa-server/src/Nasa.Pathfinder.Consumers/Internal/BotInvalidCommandConsumer.cs index 34fe692..244fc16 100644 --- a/src/nasa-server/src/Nasa.Pathfinder.Consumers/Internal/BotInvalidCommandConsumer.cs +++ b/src/nasa-server/src/Nasa.Pathfinder.Consumers/Internal/BotInvalidCommandConsumer.cs @@ -10,7 +10,7 @@ internal class BotInvalidCommandConsumer(IOperatorProcessor processor) : IBotCon public Task Consume(InvalidCommand command, CancellationToken ct = default) { var request = new SendMessageRequest(command.ClientId, command.BotId, command.Message, true, - true, string.Empty); + true); processor.SendMessage(request); diff --git a/src/nasa-server/src/Nasa.Pathfinder.Consumers/Internal/BotStandConsumer.cs b/src/nasa-server/src/Nasa.Pathfinder.Consumers/Internal/BotStandConsumer.cs index 56b673e..6284bfc 100644 --- a/src/nasa-server/src/Nasa.Pathfinder.Consumers/Internal/BotStandConsumer.cs +++ b/src/nasa-server/src/Nasa.Pathfinder.Consumers/Internal/BotStandConsumer.cs @@ -14,7 +14,7 @@ public async Task Consume(StandCommand command, CancellationToken ct = default) { var notificationText = messageDecoder.EncodeBotMessage(command.CurrentPosition, false); var request = new SendMessageRequest(command.ClientId, command.BotId, notificationText, false, - false, string.Empty); + false); processor.SendMessage(request); } } \ No newline at end of file diff --git a/src/nasa-server/src/Nasa.Pathfinder.Consumers/Internal/BotWalkerConsumer.cs b/src/nasa-server/src/Nasa.Pathfinder.Consumers/Internal/BotWalkerConsumer.cs index f535a4e..a7b45ff 100644 --- a/src/nasa-server/src/Nasa.Pathfinder.Consumers/Internal/BotWalkerConsumer.cs +++ b/src/nasa-server/src/Nasa.Pathfinder.Consumers/Internal/BotWalkerConsumer.cs @@ -17,7 +17,7 @@ public async Task Consume(WalkCommand command, CancellationToken ct = default) await repository.ChangeBotPositionAsync(command.BotId, command.CurrentPosition, ct); var notificationText = messageDecoder.EncodeBotMessage(command.CurrentPosition, false); var request = new SendMessageRequest(command.ClientId, command.BotId, notificationText, false, - false, string.Empty); + false); processor.SendMessage(request); } } \ No newline at end of file diff --git a/src/nasa-server/src/Nasa.Pathfinder.Data.Contracts/Repositories/IBotRepository.cs b/src/nasa-server/src/Nasa.Pathfinder.Data.Contracts/Repositories/IBotRepository.cs index 5ca7615..5240fd6 100644 --- a/src/nasa-server/src/Nasa.Pathfinder.Data.Contracts/Repositories/IBotRepository.cs +++ b/src/nasa-server/src/Nasa.Pathfinder.Data.Contracts/Repositories/IBotRepository.cs @@ -7,8 +7,9 @@ namespace Nasa.Pathfinder.Data.Contracts.Repositories; public interface IBotRepository { + Task AddRangeAsync(IEnumerable bots, CancellationToken ct = default); Task TryGetAsync(string botId, CancellationToken ct = default); - Task> GetBotsAsync(CancellationToken ct = default); + Task> GetBotsAsync(string mapId, CancellationToken ct = default); Task ChangeBotStatusAsync(string botId, BotStatus status, CancellationToken ct = default); Task ChangeBotPositionAsync(string botId, Position position, CancellationToken ct = default); } \ No newline at end of file diff --git a/src/nasa-server/src/Nasa.Pathfinder.Data.Contracts/Repositories/IMapRepository.cs b/src/nasa-server/src/Nasa.Pathfinder.Data.Contracts/Repositories/IMapRepository.cs index dcc3eb8..95170cc 100644 --- a/src/nasa-server/src/Nasa.Pathfinder.Data.Contracts/Repositories/IMapRepository.cs +++ b/src/nasa-server/src/Nasa.Pathfinder.Data.Contracts/Repositories/IMapRepository.cs @@ -5,4 +5,5 @@ namespace Nasa.Pathfinder.Data.Contracts.Repositories; public interface IMapRepository { Task TryGetAsync(string id, CancellationToken ct = default); + Task AddAsync(MapInfo map, CancellationToken ct = default); } \ No newline at end of file diff --git a/src/nasa-server/src/Nasa.Pathfinder.Data/Internal/BotRepository.cs b/src/nasa-server/src/Nasa.Pathfinder.Data/Internal/BotRepository.cs index 5bc6a7a..462964f 100644 --- a/src/nasa-server/src/Nasa.Pathfinder.Data/Internal/BotRepository.cs +++ b/src/nasa-server/src/Nasa.Pathfinder.Data/Internal/BotRepository.cs @@ -10,15 +10,33 @@ namespace Nasa.Pathfinder.Data.Internal; internal class BotRepository(IDataContext dataContext) : IBotRepository { + public async Task AddRangeAsync(IEnumerable bots, CancellationToken ct = default) + { + await dataContext.AcquireAsync("all", ct); + try + { + foreach (var bot in bots) + { + bot.ETag = Guid.NewGuid(); + } + + await dataContext.PushManyAsync(bots, ct); + } + finally + { + await dataContext.ReleaseAsync("all", CancellationToken.None); + } + } + public async Task TryGetAsync(string botId, CancellationToken ct = default) { return await dataContext.GetAsync(botId, ct); } - public async Task> GetBotsAsync(CancellationToken ct = default) + public async Task> GetBotsAsync(string mapId, CancellationToken ct = default) { var bots = await dataContext.GetAllAsync(ct); - return bots; + return bots.Where(x => x.MapId == mapId); } public async Task ChangeBotStatusAsync(string botId, BotStatus status, CancellationToken ct = default) diff --git a/src/nasa-server/src/Nasa.Pathfinder.Data/Internal/MapRepository.cs b/src/nasa-server/src/Nasa.Pathfinder.Data/Internal/MapRepository.cs index a750ffe..fef2c9b 100644 --- a/src/nasa-server/src/Nasa.Pathfinder.Data/Internal/MapRepository.cs +++ b/src/nasa-server/src/Nasa.Pathfinder.Data/Internal/MapRepository.cs @@ -10,4 +10,13 @@ internal class MapRepository(IDataContext context) : IMapRepository { return await context.GetAsync(id, ct); } + + public async Task AddAsync(MapInfo map, CancellationToken ct = default) + { + map.Id = Guid.NewGuid().ToString(); + map.ETag = Guid.NewGuid(); + + await context.PushAsync(map, ct); + return map; + } } \ No newline at end of file diff --git a/src/nasa-server/src/Nasa.Pathfinder.Domain/Entities/Bots/Bot.cs b/src/nasa-server/src/Nasa.Pathfinder.Domain/Entities/Bots/Bot.cs index ed0e159..ba1d847 100644 --- a/src/nasa-server/src/Nasa.Pathfinder.Domain/Entities/Bots/Bot.cs +++ b/src/nasa-server/src/Nasa.Pathfinder.Domain/Entities/Bots/Bot.cs @@ -12,5 +12,4 @@ public class Bot : IEntity public BotStatus Status { get; set; } public Position Position { get; set; } public string MapId { get; set; } - public string LastWords { get; set; } } \ No newline at end of file diff --git a/src/nasa-server/src/Nasa.Pathfinder.Domain/Interactions/BotCommands.cs b/src/nasa-server/src/Nasa.Pathfinder.Domain/Interactions/BotCommands.cs index e7f1712..97ffd9c 100644 --- a/src/nasa-server/src/Nasa.Pathfinder.Domain/Interactions/BotCommands.cs +++ b/src/nasa-server/src/Nasa.Pathfinder.Domain/Interactions/BotCommands.cs @@ -30,7 +30,6 @@ public record DeadCommand( string ClientId, string BotId, string MapId, - string LastWords, Position CurrentPosition, Guid CorrelationId = default) : IBotCommand; diff --git a/src/nasa-server/src/Nasa.Pathfinder.Facades.Contracts/IBotFacade.cs b/src/nasa-server/src/Nasa.Pathfinder.Facades.Contracts/IBotFacade.cs index 6d7fb96..29d511d 100644 --- a/src/nasa-server/src/Nasa.Pathfinder.Facades.Contracts/IBotFacade.cs +++ b/src/nasa-server/src/Nasa.Pathfinder.Facades.Contracts/IBotFacade.cs @@ -1,11 +1,12 @@ -using Nasa.Pathfinder.Domain.Bots; +using System.Numerics; using Nasa.Pathfinder.Domain.Entities.Bots; namespace Nasa.Pathfinder.Facades.Contracts; public interface IBotFacade { - Task> GetBotsAsync(CancellationToken ct = default); + Task CreateWorldAsync(Vector2 coordinates, IEnumerable bots, CancellationToken ct = default); + Task> GetBotsAsync(string mapId, CancellationToken ct = default); Task SelectBotAsync(string botId, CancellationToken ct = default); Task ResetBotAsync(string botId, CancellationToken ct = default); } \ No newline at end of file diff --git a/src/nasa-server/src/Nasa.Pathfinder.Facades/Internal/BotFacade.cs b/src/nasa-server/src/Nasa.Pathfinder.Facades/Internal/BotFacade.cs index e228411..b3e019c 100644 --- a/src/nasa-server/src/Nasa.Pathfinder.Facades/Internal/BotFacade.cs +++ b/src/nasa-server/src/Nasa.Pathfinder.Facades/Internal/BotFacade.cs @@ -1,19 +1,42 @@ +using System.Numerics; using Nasa.Pathfinder.Data.Contracts.Exceptions; using Nasa.Pathfinder.Data.Contracts.Repositories; using Nasa.Pathfinder.Domain.Bots; using Nasa.Pathfinder.Domain.Entities.Bots; +using Nasa.Pathfinder.Domain.Entities.World; using Nasa.Pathfinder.Facades.Contracts; using Nasa.Pathfinder.Facades.Contracts.Exceptions; namespace Nasa.Pathfinder.Facades.Internal; -internal class BotFacade(IBotRepository repository) : IBotFacade +internal class BotFacade( + IBotRepository repository, + IMapRepository mapRepository) : IBotFacade { - public async Task> GetBotsAsync(CancellationToken ct = default) + public async Task CreateWorldAsync(Vector2 coordinates, IEnumerable bots, CancellationToken ct = default) + { + var map = new MapInfo + { + SizeX = (int)coordinates.X, + SizeY = (int)coordinates.Y, + }; + var createdMap = await mapRepository.AddAsync(map, ct); + + foreach (var bot in bots) + { + bot.MapId = createdMap.Id; + } + + await repository.AddRangeAsync(bots.ToList(), ct); + + return createdMap.Id; + } + + public async Task> GetBotsAsync(string mapId, CancellationToken ct = default) { try { - var bots = await repository.GetBotsAsync(ct); + var bots = await repository.GetBotsAsync(mapId, ct); return bots; } catch (Exception ex) when (ex is OperationCanceledException) diff --git a/src/nasa-server/src/Nasa.Pathfinder.Infrastructure.Contracts/DataContexts/IDataContext.cs b/src/nasa-server/src/Nasa.Pathfinder.Infrastructure.Contracts/DataContexts/IDataContext.cs index f2e5ec7..d874bea 100644 --- a/src/nasa-server/src/Nasa.Pathfinder.Infrastructure.Contracts/DataContexts/IDataContext.cs +++ b/src/nasa-server/src/Nasa.Pathfinder.Infrastructure.Contracts/DataContexts/IDataContext.cs @@ -6,6 +6,7 @@ namespace Nasa.Pathfinder.Infrastructure.Contracts.DataContexts; public interface IDataContext { Task PushAsync(T entry, CancellationToken ct = default) where T : class, IEntity; + Task PushManyAsync(IEnumerable entry, CancellationToken ct = default) where T : class, IEntity; Task> UpdateAsync(T entry, CancellationToken ct = default) where T : class, IEntity; Task GetAsync(string id, CancellationToken ct = default) where T : class, IEntity; Task> GetAllAsync(CancellationToken ct = default); diff --git a/src/nasa-server/src/Nasa.Pathfinder.Infrastructure.Contracts/Grpc/Requests/SendMessageRequest.cs b/src/nasa-server/src/Nasa.Pathfinder.Infrastructure.Contracts/Grpc/Requests/SendMessageRequest.cs index 6138cf8..8ddbea1 100644 --- a/src/nasa-server/src/Nasa.Pathfinder.Infrastructure.Contracts/Grpc/Requests/SendMessageRequest.cs +++ b/src/nasa-server/src/Nasa.Pathfinder.Infrastructure.Contracts/Grpc/Requests/SendMessageRequest.cs @@ -5,5 +5,4 @@ public record SendMessageRequest( string BotId, string Message, bool IsLost, - bool IsInvalidCommand, - string LastWords); \ No newline at end of file + bool IsInvalidCommand); \ No newline at end of file diff --git a/src/nasa-server/src/Nasa.Pathfinder.Infrastructure/Internal/Memory/MemoryDataContext.cs b/src/nasa-server/src/Nasa.Pathfinder.Infrastructure/Internal/Memory/MemoryDataContext.cs index a3de2b7..be6fce5 100644 --- a/src/nasa-server/src/Nasa.Pathfinder.Infrastructure/Internal/Memory/MemoryDataContext.cs +++ b/src/nasa-server/src/Nasa.Pathfinder.Infrastructure/Internal/Memory/MemoryDataContext.cs @@ -29,6 +29,21 @@ public Task PushAsync(T entry, CancellationToken ct = default) where T : clas return Task.CompletedTask; } + + public Task PushManyAsync(IEnumerable entry, CancellationToken ct = default) where T : class, IEntity + { + if (ct.IsCancellationRequested) + return Task.CompletedTask; + + foreach (var entity in entry) + { + var id = GetInternalId(entity.Id); + cache.Set(id, entity); + _types.Add(id, entity.GetType()); + } + + return Task.CompletedTask; + } public Task> UpdateAsync(T entry, CancellationToken ct = default) where T : class, IEntity { diff --git a/src/nasa-server/src/Nasa.Pathfinder.Services/Internal/WorldMapService.cs b/src/nasa-server/src/Nasa.Pathfinder.Services/Internal/WorldMapService.cs index 301bfcd..f7912aa 100644 --- a/src/nasa-server/src/Nasa.Pathfinder.Services/Internal/WorldMapService.cs +++ b/src/nasa-server/src/Nasa.Pathfinder.Services/Internal/WorldMapService.cs @@ -19,58 +19,59 @@ public async Task ReachPosition(string mapId, Position current Y = currentPosition.Y, Direction = Direction.N }; - var funerals = await funeralRepository.GetFuneralsAsync(mapId, ct); - IPositionProject position = new PositionChanged(currentPosition); - - foreach (var command in commands) + try { - Position current; - switch (command) + var funerals = await funeralRepository.GetFuneralsAsync(mapId, ct); + + IPositionProject position = new PositionChanged(currentPosition); + + foreach (var command in commands) { - case MoveRight or MoveLeft: - current = position.Position; - - current.Direction = Rotate(command, current.Direction); - position = new PositionChanged(current); - - Console.WriteLine($"Coordinates: {position.Position.X} x {position.Position.Y}, " + - $"{position.Position.Direction}"); - break; - case MoveFront moveFrontCommand: - current = position.Position; - - var newPosition = await CalculateMoveFront(current, moveFrontCommand, - funerals, mapId, ct); - - if (newPosition is PositionNotChanged) - { - Console.WriteLine($"Coordinates: {current.X} x {current.Y}, {current.Direction}"); + Position current; + switch (command) + { + case MoveRight or MoveLeft: + current = position.Position; + + current.Direction = Rotate(command, current.Direction); + position = new PositionChanged(current); + break; + case MoveFront moveFrontCommand: + current = position.Position; + + var newPosition = await CalculateMoveFront(current, moveFrontCommand, + funerals, mapId, ct); + + if (newPosition is PositionNotChanged) + { + continue; + } + + if (newPosition is PositionOutOfMap @out) + { + return @out; + } + + position = newPosition; + break; + default: continue; - } - - if (newPosition is PositionOutOfMap @out) - { - Console.WriteLine($"Coordinates: {newPosition.Position.X} x {newPosition.Position.Y}, " + - $"{newPosition.Position.Direction}"); - return @out; - } - - position = newPosition; - Console.WriteLine($"Coordinates: {position.Position.X} x {position.Position.Y}, {position.Position.Direction}"); - break; - default: - continue; + } } - } - if (position.Position.X == start.X && position.Position.Y == start.Y && - position.Position.Direction == start.Direction) + if (position.Position.X == start.X && position.Position.Y == start.Y && + position.Position.Direction == start.Direction) + { + return new PositionNotChanged(currentPosition); + } + + return position; + } + catch (Exception e) { - return new PositionNotChanged(currentPosition); + return new PositionNotChanged(start); } - - return position; } private async Task CalculateMoveFront(Position position, MoveFront command, diff --git a/src/nasa-server/src/Nasa.Pathfinder/Background/MigrationBackgroundTask.cs b/src/nasa-server/src/Nasa.Pathfinder/Background/MigrationBackgroundTask.cs index c1cac28..4b946dc 100644 --- a/src/nasa-server/src/Nasa.Pathfinder/Background/MigrationBackgroundTask.cs +++ b/src/nasa-server/src/Nasa.Pathfinder/Background/MigrationBackgroundTask.cs @@ -1,3 +1,4 @@ +using Nasa.Pathfinder.Data.Contracts.Repositories; using Nasa.Pathfinder.Domain.Bots; using Nasa.Pathfinder.Domain.Entities.Bots; using Nasa.Pathfinder.Domain.Entities.World; @@ -8,11 +9,12 @@ namespace Nasa.Pathfinder.Background; public class MigrationBackgroundTask( + IBotRepository botRepository, IMemoryDataContext context) : IHostedService { public async Task StartAsync(CancellationToken cancellationToken) { - var mapId = Guid.CreateVersion7().ToString(); + var mapId = "1"; var mapInfo = new MapInfo { Id = mapId, @@ -23,53 +25,51 @@ public async Task StartAsync(CancellationToken cancellationToken) await context.PushAsync(mapInfo, cancellationToken); - await context.PushAsync(new Bot + var bots = new List(); + bots.Add(new Bot { Id = Guid.NewGuid().ToString(), ETag = Guid.NewGuid(), Name = "bot-1", Status = BotStatus.Available, MapId = mapId, - LastWords = "beep-bep-bep-beee.", Position = new Position { X = 1, Y = 1, Direction = Direction.E } - }, cancellationToken); - - await context.PushAsync(new Bot + }); + bots.Add(new Bot { Id = Guid.NewGuid().ToString(), ETag = Guid.NewGuid(), Name = "bot-2", Status = BotStatus.Available, MapId = mapId, - LastWords = "There is only the Emperor, and he is our shield and protector.", Position = new Position { X = 3, Y = 2, Direction = Direction.N } - }, cancellationToken); - - await context.PushAsync(new Bot + }); + bots.Add(new Bot { Id = Guid.NewGuid().ToString(), ETag = Guid.NewGuid(), Name = "bot-3", Status = BotStatus.Available, MapId = mapId, - LastWords = "Blessed is the mind too small for doubt.", Position = new Position { X = 0, Y = 3, Direction = Direction.W } - }, cancellationToken); + }); + + await botRepository.AddRangeAsync(bots, cancellationToken); } public Task StopAsync(CancellationToken cancellationToken) diff --git a/src/nasa-server/src/Nasa.Pathfinder/Hubs/MessageHub.cs b/src/nasa-server/src/Nasa.Pathfinder/Hubs/MessageHub.cs index 2fd355e..aa19501 100644 --- a/src/nasa-server/src/Nasa.Pathfinder/Hubs/MessageHub.cs +++ b/src/nasa-server/src/Nasa.Pathfinder/Hubs/MessageHub.cs @@ -67,8 +67,7 @@ private async Task NotifyClient(SendMessageRequest message) BotId = message.BotId, Message = message.Message, IsLost = message.IsLost, - IsInvalidCommand = message.IsInvalidCommand, - LastWords = message.LastWords + IsInvalidCommand = message.IsInvalidCommand }; await stream.WriteAsync(reply); diff --git a/src/nasa-server/src/Nasa.Pathfinder/Services/PathfinderGrpcService.cs b/src/nasa-server/src/Nasa.Pathfinder/Services/PathfinderGrpcService.cs index 6aeb9bb..51385f1 100644 --- a/src/nasa-server/src/Nasa.Pathfinder/Services/PathfinderGrpcService.cs +++ b/src/nasa-server/src/Nasa.Pathfinder/Services/PathfinderGrpcService.cs @@ -1,3 +1,4 @@ +using System.Numerics; using Google.Protobuf.WellKnownTypes; using Grpc.Core; using Nasa.Pathfinder.Domain.Messages; @@ -25,9 +26,17 @@ public override async Task Ping(Empty request, ServerCallContext c }; } - public override async Task GetBots(Empty request, ServerCallContext context) + public override async Task CreateWorld(CreateWorldRequest request, ServerCallContext context) { - var result = await botFacade.GetBotsAsync(context.CancellationToken); + var vector = new Vector2(request.SizeX, request.SizeY); + var bots = Request.MapFromCreateWorldRequest(request); + var result = await botFacade.CreateWorldAsync(vector, bots, context.CancellationToken); + return Response.MapToCreateWorldResponse(result); + } + + public override async Task GetBots(GetBotsRequest request, ServerCallContext context) + { + var result = await botFacade.GetBotsAsync(request.MapId, context.CancellationToken); var response = Response.MapToGetBotsResponse(result); return response; diff --git a/src/nasa-server/src/Nasa.Pathfinder/Types/Request.cs b/src/nasa-server/src/Nasa.Pathfinder/Types/Request.cs new file mode 100644 index 0000000..abc33ad --- /dev/null +++ b/src/nasa-server/src/Nasa.Pathfinder/Types/Request.cs @@ -0,0 +1,28 @@ +using Nasa.Pathfinder.Domain.Bots; +using Nasa.Pathfinder.Domain.World; +using Pathfinder.Messages; +using Bot = Nasa.Pathfinder.Domain.Entities.Bots.Bot; +using Position = Nasa.Pathfinder.Domain.World.Position; + + +namespace Nasa.Pathfinder.Types; + +public static class Request +{ + public static IEnumerable MapFromCreateWorldRequest(CreateWorldRequest request) + { + return request.Bots.Select(bot => new Bot + { + Id = Guid.NewGuid().ToString(), + ETag = Guid.NewGuid(), + Name = bot.Name, + Status = BotStatus.Available, + Position = new Position + { + X = bot.Position.X, + Y = bot.Position.Y, + Direction = Enum.Parse(bot.Position.Direction), + } + }).ToList(); + } +} \ No newline at end of file diff --git a/src/nasa-server/src/Nasa.Pathfinder/Types/Response.cs b/src/nasa-server/src/Nasa.Pathfinder/Types/Response.cs index 055fa0e..89b7ac5 100644 --- a/src/nasa-server/src/Nasa.Pathfinder/Types/Response.cs +++ b/src/nasa-server/src/Nasa.Pathfinder/Types/Response.cs @@ -68,4 +68,12 @@ public static ResetBotResponse MapToResetBotResponse(Domain.Entities.Bots.Bot bo }; return response; } + + public static CreateWorldResponse MapToCreateWorldResponse(string mapId) + { + return new CreateWorldResponse + { + MapId = mapId + }; + } } \ No newline at end of file diff --git a/src/nasa-server/tests/Nasa.Pathfinder.Facades.Tests/BotFacadeTests.cs b/src/nasa-server/tests/Nasa.Pathfinder.Facades.Tests/BotFacadeTests.cs index b91baf4..0eb9cc7 100644 --- a/src/nasa-server/tests/Nasa.Pathfinder.Facades.Tests/BotFacadeTests.cs +++ b/src/nasa-server/tests/Nasa.Pathfinder.Facades.Tests/BotFacadeTests.cs @@ -20,6 +20,7 @@ public void Setup() } private Mock _repositoryMock; + private Mock _mapRepositoryMock; [Test] public async Task GetBotsAsync_RepoReturnsNothing_ShouldReturnEmptyList() @@ -27,11 +28,11 @@ public async Task GetBotsAsync_RepoReturnsNothing_ShouldReturnEmptyList() await TestRunner> .Arrange(() => { - _repositoryMock.Setup(x => x.GetBotsAsync(It.IsAny())) + _repositoryMock.Setup(x => x.GetBotsAsync( It.IsAny(), It.IsAny())) .ReturnsAsync([]); - return new BotFacade(_repositoryMock.Object); + return new BotFacade(_repositoryMock.Object, _mapRepositoryMock.Object); }) - .ActAsync(sut => sut.GetBotsAsync(CancellationToken.None)) + .ActAsync(sut => sut.GetBotsAsync(It.IsAny(), CancellationToken.None)) .ThenAssertAsync(result => result.Should().NotBeNull()); } @@ -41,11 +42,11 @@ public async Task GetBotsAsync_RequestAborted_ShouldReturnEmptyList() await TestRunner> .Arrange(() => { - _repositoryMock.Setup(x => x.GetBotsAsync(It.IsAny())) + _repositoryMock.Setup(x => x.GetBotsAsync(It.IsAny(),It.IsAny())) .Callback(() => throw new OperationCanceledException()); - return new BotFacade(_repositoryMock.Object); + return new BotFacade(_repositoryMock.Object, _mapRepositoryMock.Object); }) - .ActAsync(sut => sut.GetBotsAsync(CancellationToken.None)) + .ActAsync(sut => sut.GetBotsAsync(It.IsAny(), CancellationToken.None)) .ThenAssertAsync(result => result.Should().BeEmpty()); } @@ -58,7 +59,7 @@ await TestRunner _repositoryMock.Setup(x => x.ChangeBotStatusAsync(It.IsAny(), BotStatus.Acquired, It.IsAny())) .ReturnsAsync(new Bot()); - return new BotFacade(_repositoryMock.Object); + return new BotFacade(_repositoryMock.Object, _mapRepositoryMock.Object); }) .ActAsync(sut => sut.SelectBotAsync(Guid.NewGuid().ToString(), CancellationToken.None)) .ThenAssertAsync(result => result.Should().NotBeNull()); @@ -73,7 +74,7 @@ await TestRunner _repositoryMock.Setup(x => x.ChangeBotStatusAsync(It.IsAny(), BotStatus.Acquired, It.IsAny())) .Callback(() => throw new ConcurrencyException(string.Empty)); - return new BotFacade(_repositoryMock.Object); + return new BotFacade(_repositoryMock.Object, _mapRepositoryMock.Object); }) .ActAsync(sut => sut.SelectBotAsync(Guid.NewGuid().ToString(), CancellationToken.None)) .ThenAssertThrowsAsync(); @@ -88,7 +89,7 @@ await TestRunner _repositoryMock.Setup(x => x.ChangeBotStatusAsync(It.IsAny(), BotStatus.Acquired, It.IsAny())) .Callback(() => throw new DataException(string.Empty)); - return new BotFacade(_repositoryMock.Object); + return new BotFacade(_repositoryMock.Object, _mapRepositoryMock.Object); }) .ActAsync(sut => sut.SelectBotAsync(Guid.NewGuid().ToString(), CancellationToken.None)) .ThenAssertThrowsAsync(); @@ -103,7 +104,7 @@ await TestRunner _repositoryMock.Setup(x => x.ChangeBotStatusAsync(It.IsAny(), BotStatus.Acquired, It.IsAny())) .Callback(() => throw new OperationCanceledException()); - return new BotFacade(_repositoryMock.Object); + return new BotFacade(_repositoryMock.Object, _mapRepositoryMock.Object); }) .ActAsync(sut => sut.SelectBotAsync(It.IsAny(), It.IsAny())) .ThenAssertAsync(result => result.Should().NotBeNull()); @@ -118,7 +119,7 @@ await TestRunner _repositoryMock.Setup(x => x.ChangeBotStatusAsync(It.IsAny(), BotStatus.Available, It.IsAny())) .ReturnsAsync(new Bot()); - return new BotFacade(_repositoryMock.Object); + return new BotFacade(_repositoryMock.Object, _mapRepositoryMock.Object); }) .ActAsync(sut => sut.ResetBotAsync(Guid.NewGuid().ToString(), CancellationToken.None)) .ThenAssertAsync(result => result.Should().NotBeNull()); @@ -133,7 +134,7 @@ await TestRunner _repositoryMock.Setup(x => x.ChangeBotStatusAsync(It.IsAny(), BotStatus.Available, It.IsAny())) .Callback(() => throw new ConcurrencyException(string.Empty)); - return new BotFacade(_repositoryMock.Object); + return new BotFacade(_repositoryMock.Object, _mapRepositoryMock.Object); }) .ActAsync(sut => sut.ResetBotAsync(Guid.NewGuid().ToString(), CancellationToken.None)) .ThenAssertThrowsAsync(); @@ -148,7 +149,7 @@ await TestRunner _repositoryMock.Setup(x => x.ChangeBotStatusAsync(It.IsAny(), BotStatus.Available, It.IsAny())) .Callback(() => throw new DataException(string.Empty)); - return new BotFacade(_repositoryMock.Object); + return new BotFacade(_repositoryMock.Object, _mapRepositoryMock.Object); }) .ActAsync(sut => sut.ResetBotAsync(Guid.NewGuid().ToString(), CancellationToken.None)) .ThenAssertThrowsAsync(); @@ -163,7 +164,7 @@ await TestRunner _repositoryMock.Setup(x => x.ChangeBotStatusAsync(It.IsAny(), BotStatus.Available, It.IsAny())) .Callback(() => throw new OperationCanceledException()); - return new BotFacade(_repositoryMock.Object); + return new BotFacade(_repositoryMock.Object, _mapRepositoryMock.Object); }) .ActAsync(sut => sut.ResetBotAsync(It.IsAny(), It.IsAny())) .ThenAssertAsync(result => result.Should().NotBeNull()); diff --git a/src/nasa-server/tests/Nasa.Pathfinder.Services.Tests/WorldMapServiceTests.cs b/src/nasa-server/tests/Nasa.Pathfinder.Services.Tests/WorldMapServiceTests.cs index 86110eb..52c4729 100644 --- a/src/nasa-server/tests/Nasa.Pathfinder.Services.Tests/WorldMapServiceTests.cs +++ b/src/nasa-server/tests/Nasa.Pathfinder.Services.Tests/WorldMapServiceTests.cs @@ -5,7 +5,6 @@ using Nasa.Pathfinder.Domain.Interactions; using Nasa.Pathfinder.Domain.World; using Nasa.Pathfinder.Services.Internal; -using Nasa.Pathfinder.Services.Tests.Fakes; using Nasa.Pathfinder.Tests; using NUnit.Framework; @@ -23,8 +22,7 @@ public void Setup() private Mock _mockMapRepository; private Mock _mockFuneralRepository; - - + [Test] public async Task ReachPosition_WhenFuneralWithDesiredPositionFindAndNotOutOfMap_ShouldReturnNotChanged() { @@ -231,283 +229,4 @@ await TestRunner }); } - - - // - // [Test] - // public async Task ReachPosition_WhenFuneralWithDesiredPositionOutOfMap_ShouldReturnNotChanged() - // { - // var mapId = $"map_{new Faker().Database.Random.Uuid()}"; - // - // var desiredPosition = new Position - // { - // X = 51, - // Y = 3, - // Direction = Direction.W - // }; - // - // await TestRunner - // .Arrange(() => - // { - // _mockMapRepository.Setup(x => x.TryGetAsync(mapId, It.IsAny())) - // .ReturnsAsync(new MapInfo - // { - // Id = mapId, - // ETag = new Faker().Random.Guid(), - // SizeX = 50, - // SizeY = 50, - // }); - // - // _mockFuneralRepository.Setup(x => - // x.GetFuneralsAsync(mapId, It.IsAny())) - // .ReturnsAsync(new List { FakeFuneral.Make(mapId) }); - // - // return new WorldMapService(_mockMapRepository.Object, _mockFuneralRepository.Object); - // }) - // .ActAsync(async sut => await sut.TryReachPosition(mapId, desiredPosition)) - // .ThenAssertAsync(position => { Assert.That(position, Is.TypeOf()); }); - // } - // - // [Test] - // public async Task ReachPosition_WhenBotCanMove_ShouldReturnChanged() - // { - // var mapId = $"map_{new Faker().Database.Random.Uuid()}"; - // - // var desiredPosition = new Position - // { - // X = 4, - // Y = 3, - // Direction = Direction.W - // }; - // - // await TestRunner - // .Arrange(() => - // { - // _mockMapRepository.Setup(x => x.TryGetAsync(mapId, It.IsAny())) - // .ReturnsAsync(new MapInfo - // { - // Id = mapId, - // ETag = new Faker().Random.Guid(), - // SizeX = 50, - // SizeY = 50, - // }); - // - // _mockFuneralRepository.Setup(x => - // x.GetFuneralsAsync(mapId, It.IsAny())) - // .ReturnsAsync(new List { FakeFuneral.Make(mapId) }); - // - // return new WorldMapService(_mockMapRepository.Object, _mockFuneralRepository.Object); - // }) - // .ActAsync(async sut => await sut.TryReachPosition(mapId, desiredPosition)) - // .ThenAssertAsync(position => { Assert.That(position, Is.TypeOf()); }); - // } - // - // - // [Test] - // public void CalculateDesiredPosition_WhenWeGotListOfCommands_ShouldReturnsDestination() - // { - // TestRunner - // .Arrange(() => new WorldMapService(_mockMapRepository.Object, _mockFuneralRepository.Object)) - // .Act(sut => - // { - // var currentPosition = new Position - // { - // X = 1, - // Y = 1, - // Direction = Direction.E - // }; - // - // var commands = new Stack(); - // commands.Push(new MoveFront()); - // commands.Push(new MoveRight()); - // commands.Push(new MoveFront()); - // commands.Push(new MoveRight()); - // commands.Push(new MoveFront()); - // commands.Push(new MoveRight()); - // commands.Push(new MoveFront()); - // commands.Push(new MoveRight()); - // - // return sut.CalculateDesiredPosition(currentPosition, commands); - // }) - // .Assert(position => - // { - // Assert.Multiple(() => - // { - // Assert.That(position.X, Is.EqualTo(1)); - // Assert.That(position.Y, Is.EqualTo(1)); - // Assert.That(position.Direction, Is.EqualTo(Direction.E)); - // }); - // }); - // } - // - // [Test] - // public void CalculateDesiredPosition_WhenDesiredPositionOutOfGrid_ShouldReturnsDestination() - // { - // TestRunner - // .Arrange(() => new WorldMapService(_mockMapRepository.Object, _mockFuneralRepository.Object)) - // .Act(sut => - // { - // var currentPosition = new Position - // { - // X = 0, - // Y = 3, - // Direction = Direction.W - // }; - // - // _mockFuneralRepository.Setup(x => x.GetFuneralsAsync(It.IsAny(), It.IsAny())) - // .ReturnsAsync([ - // new Funeral - // { - // Id = "1", - // ETag = Guid.NewGuid(), - // MapId = "s", - // Value = new Position - // { - // X = 2, - // Y = 4, - // Direction = Direction.E - // } - // } - // ]); - // - // var commands = new Stack(); - // commands.Push(new MoveLeft()); - // - // commands.Push(new MoveFront()); - // commands.Push(new MoveLeft()); - // - // commands.Push(new MoveFront()); - // commands.Push(new MoveLeft()); - // - // commands.Push(new MoveFront()); - // commands.Push(new MoveFront()); - // commands.Push(new MoveFront()); - // - // commands.Push(new MoveLeft()); - // commands.Push(new MoveLeft()); - // - // - // return sut.CalculateDesiredPosition(currentPosition, commands); - // }) - // .Assert(position => - // { - // Assert.Multiple(() => - // { - // Assert.That(position.X, Is.EqualTo(2)); - // Assert.That(position.Y, Is.EqualTo(4)); - // Assert.That(position.Direction, Is.EqualTo(Direction.S)); - // }); - // }); - // } - // - // [Test] - // public async Task TryReachPosition_WhenFuneralWithDesiredPositionFindAndNotOutOfMap_ShouldReturnNotChanged() - // { - // var mapId = $"map_{new Faker().Database.Random.Uuid()}"; - // - // var desiredPosition = new Position - // { - // X = 2, - // Y = 4, - // Direction = Direction.S - // }; - // - // await TestRunner - // .Arrange(() => - // { - // _mockMapRepository.Setup(x => x.TryGetAsync(mapId, It.IsAny())) - // .ReturnsAsync(new MapInfo - // { - // Id = mapId, - // ETag = new Faker().Random.Guid(), - // SizeX = 5, - // SizeY = 3, - // }); - // - // _mockFuneralRepository.Setup(x => - // x.GetFuneralsAsync(mapId, It.IsAny())) - // .ReturnsAsync(new List - // { - // new() - // { - // Id = new Faker().Random.Guid().ToString(), - // ETag = new Faker().Random.Guid(), - // MapId = mapId, - // Value = desiredPosition - // } - // }); - // - // return new WorldMapService(_mockMapRepository.Object, _mockFuneralRepository.Object); - // }) - // .ActAsync(async sut => await sut.TryReachPosition(mapId, desiredPosition)) - // .ThenAssertAsync(position => { Assert.That(position, Is.TypeOf()); }); - // } - // - // [Test] - // public async Task TryReachPosition_WhenFuneralWithDesiredPositionOutOfMap_ShouldReturnNotChanged() - // { - // var mapId = $"map_{new Faker().Database.Random.Uuid()}"; - // - // var desiredPosition = new Position - // { - // X = 51, - // Y = 3, - // Direction = Direction.W - // }; - // - // await TestRunner - // .Arrange(() => - // { - // _mockMapRepository.Setup(x => x.TryGetAsync(mapId, It.IsAny())) - // .ReturnsAsync(new MapInfo - // { - // Id = mapId, - // ETag = new Faker().Random.Guid(), - // SizeX = 50, - // SizeY = 50, - // }); - // - // _mockFuneralRepository.Setup(x => - // x.GetFuneralsAsync(mapId, It.IsAny())) - // .ReturnsAsync(new List { FakeFuneral.Make(mapId) }); - // - // return new WorldMapService(_mockMapRepository.Object, _mockFuneralRepository.Object); - // }) - // .ActAsync(async sut => await sut.TryReachPosition(mapId, desiredPosition)) - // .ThenAssertAsync(position => { Assert.That(position, Is.TypeOf()); }); - // } - // - // [Test] - // public async Task TryReachPosition_WhenBotCanMove_ShouldReturnChanged() - // { - // var mapId = $"map_{new Faker().Database.Random.Uuid()}"; - // - // var desiredPosition = new Position - // { - // X = 4, - // Y = 3, - // Direction = Direction.W - // }; - // - // await TestRunner - // .Arrange(() => - // { - // _mockMapRepository.Setup(x => x.TryGetAsync(mapId, It.IsAny())) - // .ReturnsAsync(new MapInfo - // { - // Id = mapId, - // ETag = new Faker().Random.Guid(), - // SizeX = 50, - // SizeY = 50, - // }); - // - // _mockFuneralRepository.Setup(x => - // x.GetFuneralsAsync(mapId, It.IsAny())) - // .ReturnsAsync(new List { FakeFuneral.Make(mapId) }); - // - // return new WorldMapService(_mockMapRepository.Object, _mockFuneralRepository.Object); - // }) - // .ActAsync(async sut => await sut.TryReachPosition(mapId, desiredPosition)) - // .ThenAssertAsync(position => { Assert.That(position, Is.TypeOf()); }); - // } } \ No newline at end of file diff --git a/tests/integration/Nasa.IntegrationTests.Pathfinder/PathfinderServiceTests.cs b/tests/integration/Nasa.IntegrationTests.Pathfinder/PathfinderServiceTests.cs index f4ca1f7..3501212 100644 --- a/tests/integration/Nasa.IntegrationTests.Pathfinder/PathfinderServiceTests.cs +++ b/tests/integration/Nasa.IntegrationTests.Pathfinder/PathfinderServiceTests.cs @@ -1,4 +1,5 @@ using System.Diagnostics; +using System.Numerics; using ErrorOr; using Google.Protobuf.WellKnownTypes; using Grpc.Core; @@ -29,13 +30,16 @@ public void Ping_ReturnsOk_HappyPath() .Act(sut => sut.Ping(new Empty())) .Assert(response => Assert.That(response.IsSuccessful, Is.True)); } - + [Test] public void GetBots_ReturnsBotList_ShouldReturn3Bots() { TestRunner .Arrange(() => new Client(_channel)) - .Act(sut => sut.GetBots(new Empty())) + .Act(sut => sut.GetBots(new GetBotsRequest + { + MapId = "1" + })) .Assert(response => { Assert.Multiple(() => @@ -46,7 +50,7 @@ public void GetBots_ReturnsBotList_ShouldReturn3Bots() }); }); } - + [Test] public void SelectBot_ReturnsBot_ShouldReturnBot() { @@ -54,22 +58,19 @@ public void SelectBot_ReturnsBot_ShouldReturnBot() .Arrange(() => new Client(_channel)) .Act(sut => { - var bot = sut.GetBots(new Empty()).Bots.First(); + var bot = sut.GetBots(new GetBotsRequest + { + MapId = "1" + }).Bots.First(); var result = sut.SelectBot(new SelectBotRequest { BotId = bot.Id }); return result; }) - .Assert(response => - { - Assert.Multiple(() => - { - Assert.That(response.Bot, Is.Not.Null); - }); - }); + .Assert(response => { Assert.Multiple(() => { Assert.That(response.Bot, Is.Not.Null); }); }); } - + [Test] public void SelectBot_BotAlreadyReleased_ShouldFail() { @@ -77,14 +78,17 @@ public void SelectBot_BotAlreadyReleased_ShouldFail() .Arrange(() => new Client(_channel)) .Act(sut => { - var bot = sut.GetBots(new Empty()).Bots.First(); + var bot = sut.GetBots(new GetBotsRequest + { + MapId = "1" + }).Bots.First(); try { sut.SelectBot(new SelectBotRequest { BotId = bot.Id }); - + sut.SelectBot(new SelectBotRequest { BotId = bot.Id @@ -104,7 +108,7 @@ public void SelectBot_BotAlreadyReleased_ShouldFail() Assert.That(response.StatusCode, Is.EqualTo(StatusCode.InvalidArgument)); }); } - + [Test] public void ResetBot_ReturnsBot_ShouldInvalidArgument() { @@ -112,7 +116,10 @@ public void ResetBot_ReturnsBot_ShouldInvalidArgument() .Arrange(() => new Client(_channel)) .Act(sut => { - var bot = sut.GetBots(new Empty()).Bots.First(); + var bot = sut.GetBots(new GetBotsRequest + { + MapId = "1" + }).Bots.First(); bot = sut.SelectBot(new SelectBotRequest { BotId = bot.Id @@ -122,18 +129,12 @@ public void ResetBot_ReturnsBot_ShouldInvalidArgument() { BotId = bot.Id }); - + return response; }) - .Assert(response => - { - Assert.Multiple(() => - { - Assert.That(response.Bot, Is.Not.Null); - }); - }); + .Assert(response => { Assert.Multiple(() => { Assert.That(response.Bot, Is.Not.Null); }); }); } - + [Test] public void ResetBot_BotIsFree_ShouldInvalidArgument() { @@ -141,7 +142,10 @@ public void ResetBot_BotIsFree_ShouldInvalidArgument() .Arrange(() => new Client(_channel)) .Act(sut => { - var bot = sut.GetBots(new Empty()).Bots.First(); + var bot = sut.GetBots(new GetBotsRequest + { + MapId = "1" + }).Bots.First(); bot = sut.SelectBot(new SelectBotRequest { BotId = bot.Id @@ -158,7 +162,7 @@ public void ResetBot_BotIsFree_ShouldInvalidArgument() { BotId = bot.Id }); - + return null; } catch (RpcException ex) @@ -178,7 +182,7 @@ public void ResetBot_BotIsFree_ShouldInvalidArgument() } [Test] - [Ignore("Right now, it's only for manual starting")] + //[Ignore("Right now, it's only for manual starting")] public async Task SendMessage_ApplyTestTaskBehavior_ShouldReturn3DifferentMessages() { const string message1 = "RFRFRFRF"; @@ -192,10 +196,29 @@ public async Task SendMessage_ApplyTestTaskBehavior_ShouldReturn3DifferentMessag { message3, new Position { X = 2, Y = 4, Direction = "S" } }, }; + var mapId = string.Empty; + + string[] botNames = ["bot-12", "bot-14", "bot-15"]; + await TestRunner>> - .Arrange(() => + .ArrangeAsync(async () => { var client = new Client(_channel); + + var createWorldResponse = await client.CreateWorldAsync(new CreateWorldRequest + { + SizeX = 5, + SizeY = 3, + Bots = + { + new Bot() { Name = botNames[0], Position = new Position { X = 1, Y = 1, Direction = "E"} }, + new Bot() { Name = botNames[1], Position = new Position { X = 3, Y = 2, Direction = "N"} }, + new Bot() { Name = botNames[2], Position = new Position { X = 0, Y = 3, Direction = "W"} } + } + }); + + mapId = createWorldResponse.MapId; + return client; }) .ActAsync(async sut => @@ -204,101 +227,56 @@ await TestRunner>> try { - //<--- 3 Bots case ---> //<--- First bot ---> - var first = sut.GetBots(new Empty()).Bots.First(x => x.Status == "Available"); + var first = sut.GetBots(new GetBotsRequest + { + MapId = mapId + }).Bots.First(x => x.Name == botNames[0]); var bot = sut.SelectBot(new SelectBotRequest { BotId = first.Id }).Bot; - - var stream = sut.SendMessage(new Metadata - { - { "TraceId", Activity.Current?.TraceId.ToString() ?? Guid.NewGuid().ToString() }, - { "clientId", Guid.NewGuid().ToString() } - }); - - Thread.Sleep(1000); - - botsMessage.Add(bot.Id, message1); - await stream.RequestStream.WriteAsync(new SendMessageRequest - { - BotId = bot.Id, - Message = message1 - }, CancellationToken.None); - - Thread.Sleep(2000); - - var cts = new CancellationTokenSource(); - var token = cts.Token; - - try - { - while (await stream.ResponseStream.MoveNext(token)) - { - var response = stream.ResponseStream.Current; - responses.Add(response); - break; - } - } - catch (Exception ex) - { - responses.Add(Error.Failure(ex.Message)); - } - - await stream.RequestStream.CompleteAsync(); + + await StartWorkflowAsync(bot, message1); //<--- Second bot ---> - var second = sut.GetBots(new Empty()).Bots.First(x => x.Status == "Available"); + var second = sut.GetBots(new GetBotsRequest + { + MapId = mapId + }).Bots.First(x => x.Name == botNames[1]); bot = sut.SelectBot(new SelectBotRequest { BotId = second.Id }).Bot; - - stream = sut.SendMessage(new Metadata - { - { "TraceId", Activity.Current?.TraceId.ToString() ?? Guid.NewGuid().ToString() }, - { "clientId", Guid.NewGuid().ToString() } - }); - - Thread.Sleep(1000); - - botsMessage.Add(bot.Id, message2); - await stream.RequestStream.WriteAsync(new SendMessageRequest - { - BotId = bot.Id, - Message = message2 - }, CancellationToken.None); - - Thread.Sleep(2000); - - try - { - while (await stream.ResponseStream.MoveNext(token)) - { - var response = stream.ResponseStream.Current; - responses.Add(response); - break; - } - } - catch (Exception ex) - { - responses.Add(Error.Failure(ex.Message)); - } - - await stream.RequestStream.CompleteAsync(); + + await StartWorkflowAsync(bot, message2); //<--- Third bot ---> - var third = sut.GetBots(new Empty()).Bots.First(x => x.Status == "Available"); + var third = sut.GetBots(new GetBotsRequest + { + MapId = mapId + }).Bots.First(x => x.Name == botNames[2]); bot = sut.SelectBot(new SelectBotRequest { BotId = third.Id }).Bot; + + await StartWorkflowAsync(bot, message3); + } + catch (Exception ex) + { + responses.Add(Error.Failure(ex.Message)); + } - stream = sut.SendMessage(new Metadata + return responses; + + + async Task StartWorkflowAsync(Bot bot, string message) + { + var stream = sut.SendMessage(new Metadata { { "TraceId", Activity.Current?.TraceId.ToString() ?? Guid.NewGuid().ToString() }, { "clientId", Guid.NewGuid().ToString() } @@ -306,15 +284,18 @@ await stream.RequestStream.WriteAsync(new SendMessageRequest Thread.Sleep(1000); - botsMessage.Add(bot.Id, message3); + botsMessage.Add(bot.Id, message); await stream.RequestStream.WriteAsync(new SendMessageRequest { BotId = bot.Id, - Message = message3 + Message = message }, CancellationToken.None); Thread.Sleep(2000); + var cts = new CancellationTokenSource(); + var token = cts.Token; + try { while (await stream.ResponseStream.MoveNext(token)) @@ -330,18 +311,12 @@ await stream.RequestStream.WriteAsync(new SendMessageRequest } await stream.RequestStream.CompleteAsync(); - } - catch (Exception ex) - { - responses.Add(Error.Failure(ex.Message)); - } - - return responses; + } }) .ThenAssertAsync(responses => { var errors = new List(); - + foreach (var response in responses) { if (response.IsError) @@ -353,61 +328,65 @@ await stream.RequestStream.WriteAsync(new SendMessageRequest try { var client = new Client(_channel); - var bots = client.GetBots(new Empty()).Bots; + var bots = client.GetBots(new GetBotsRequest + { + MapId = mapId + }).Bots; foreach (var bot in bots) { if (bot.Status == "Available") errors.Add(Error.Unexpected("Bot is available", - $"Bot with id ({bot.Id}) is still available instead of be acquired")); - - var message = botsMessage[bot.Id]; + $"Bot ({bot.Name}) is still available instead of be acquired")); - var pattern = ""; - switch (message) + if (bot.Name == botNames[0]) + { + CheckLocation(bot, new Vector2(1, 1), "E"); + } + + if (bot.Name == botNames[1]) + { + CheckLocation(bot, new Vector2(3, 3), "N"); + + if (bot.Status != "Dead") + errors.Add(Error.Unexpected("Bot is available", + $"Bot ({bot.Name}) is still available instead of be acquired")); + } + + if (bot.Name == botNames[2]) { - case message1: - pattern = message1; - break; - case message2: - pattern = message2; - break; - case message3: - pattern = message3; - if (bot.Status != "Dead") - { - errors.Add(Error.Unexpected("Incorrect status", - $"Bot with id ({bot.Id}) must dead, but it still alive!")); - } - break; + CheckLocation(bot, new Vector2(2, 3), "S"); } + } - if (bot.Position.X != botsPositions[pattern].X || bot.Position.Y != botsPositions[pattern].Y || - bot.Position.Direction != botsPositions[pattern].Direction) + void CheckLocation(Bot bot, Vector2 coordinates, string direction) + { + if (bot.Position.X != (int)coordinates.X + && bot.Position.Y != (int)coordinates.Y + && bot.Position.Direction != direction) { errors.Add(Error.Unexpected("Incorrect position", - $"Bot with id ({bot.Id}) has incorrect position")); + $"Bot ({bot.Name}) has incorrect position")); } } - } finally { if (!errors.Any()) Assert.Pass(); - + if (errors.Count > 0) { foreach (var error in errors) { Console.WriteLine("{0}: {1}", error.Code, error.Description); } - + Assert.Fail(); } } }); } - + [TearDown] public void TearDown() { diff --git a/tests/integration/Nasa.IntegrationTests/TestRunner.cs b/tests/integration/Nasa.IntegrationTests/TestRunner.cs index 02ff66b..4392c4b 100644 --- a/tests/integration/Nasa.IntegrationTests/TestRunner.cs +++ b/tests/integration/Nasa.IntegrationTests/TestRunner.cs @@ -3,18 +3,31 @@ namespace Nasa.IntegrationTests; public class TestRunner { private readonly Func _arrange; + private readonly Func> _arrangeAsync; private TResult? _result; private TSut? _sut; private TestRunner(Func arrange) { _arrange = arrange; + _arrangeAsync = null; + } + + private TestRunner(Func> arrange) + { + _arrange = null; + _arrangeAsync = arrange; } public static TestRunner Arrange(Func arrange) { return new TestRunner(arrange); } + + public static TestRunner ArrangeAsync(Func> arrange) + { + return new TestRunner(arrange); + } public TestRunner Act(Func act) { @@ -25,7 +38,7 @@ public TestRunner Act(Func act) public async Task> ActAsync(Func> act) { - _sut = _arrange(); + _sut = _arrangeAsync is null ? _arrange() : await _arrangeAsync(); _result = await act(_sut); return this; }