AStarPathFinderTests.cs 7.05 KB
using System.Reflection;
using Microsoft.Extensions.Logging.Abstractions;
using Rcs.Application.Services.PathFind.Models;
using Rcs.Domain.Entities;
using Rcs.Domain.Enums;
using Rcs.Infrastructure.PathFinding.Services;
using Xunit;

namespace Rcs.Infrastructure.Tests;

public class AStarPathFinderTests
{
    private const double OppositeDirectionCost = 9999999.0;

    [Fact]
    public void FindPath_AppliesSameDirectionCost_999()
    {
        var pathFinder = CreatePathFinder();
        var graph = CreateSimpleGraph();
        var request = CreateSimpleRequest(graph);
        var otherRobotId = Guid.NewGuid();
        var context = new GlobalPathContext
        {
            RobotPlannedEdges =
            {
                [otherRobotId] = new List<PlannedEdge>
                {
                    new()
                    {
                        FromNodeId = GetStartNode(graph),
                        ToNodeId = GetEndNode(graph),
                        FromNodeCode = "N1",
                        ToNodeCode = "N2",
                        Length = 1,
                        TraveledDistance = 0,
                        CurrentSpeed = 1,
                        AverageSpeed = 1,
                        EstimatedArrivalTime = 10
                    }
                }
            }
        };

        var result = pathFinder.FindPath(request, graph, context);

        Assert.True(result.Success);
        Assert.Equal(1000d, result.TotalCost, 6);
    }

    [Fact]
    public void FindPath_AppliesSweepEdgePenalty_AsOppositeDirectionCost()
    {
        var pathFinder = CreatePathFinder();
        var graph = CreateSimpleGraph();
        var request = CreateSimpleRequest(graph);
        var otherRobotId = Guid.NewGuid();
        var context = new GlobalPathContext
        {
            RobotSweepLockedEdgeCodes =
            {
                [otherRobotId] = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
                {
                    "E1"
                }
            }
        };

        var result = pathFinder.FindPath(request, graph, context);

        Assert.True(result.Success);
        Assert.Equal(1d + OppositeDirectionCost, result.TotalCost, 6);
    }

    [Fact]
    public void FindPath_AppliesSweepNodePenalty_AsOppositeDirectionCost()
    {
        var pathFinder = CreatePathFinder();
        var graph = CreateSimpleGraph();
        var request = CreateSimpleRequest(graph);
        var otherRobotId = Guid.NewGuid();
        var context = new GlobalPathContext
        {
            RobotSweepLockedNodeCodes =
            {
                [otherRobotId] = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
                {
                    "N1"
                }
            }
        };

        var result = pathFinder.FindPath(request, graph, context);

        Assert.True(result.Success);
        Assert.Equal(1d + OppositeDirectionCost, result.TotalCost, 6);
    }

    [Fact]
    public void FindPath_SweepEdgeAndNodeFromSameRobot_PenalizesOnlyOnce()
    {
        var pathFinder = CreatePathFinder();
        var graph = CreateSimpleGraph();
        var request = CreateSimpleRequest(graph);
        var otherRobotId = Guid.NewGuid();
        var context = new GlobalPathContext
        {
            RobotSweepLockedNodeCodes =
            {
                [otherRobotId] = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
                {
                    "N1"
                }
            },
            RobotSweepLockedEdgeCodes =
            {
                [otherRobotId] = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
                {
                    "E1"
                }
            }
        };

        var result = pathFinder.FindPath(request, graph, context);

        Assert.True(result.Success);
        Assert.Equal(1d + OppositeDirectionCost, result.TotalCost, 6);
    }

    [Fact]
    public void FindPath_WithoutPlannedAndSweepConflicts_DoesNotAddExtraPenalty()
    {
        var pathFinder = CreatePathFinder();
        var graph = CreateSimpleGraph();
        var request = CreateSimpleRequest(graph);

        var result = pathFinder.FindPath(request, graph, new GlobalPathContext());

        Assert.True(result.Success);
        Assert.Equal(1d, result.TotalCost, 6);
    }

    [Fact]
    public void ExtractLockCodesByMap_OnlyReturnsCodesInCurrentMap()
    {
        var method = typeof(AgvPathService).GetMethod(
            "ExtractLockCodesByMap",
            BindingFlags.NonPublic | BindingFlags.Static);

        Assert.NotNull(method);

        var keys = new[]
        {
            "rcs:lock:node:MAP-A:N1",
            "rcs:lock:node:MAP-B:N2",
            "rcs:lock:edge:MAP-A:E1",
            "rcs:lock:edge:MAP-C:E9"
        };

        var result = (HashSet<string>)method!.Invoke(null, new object[] { keys, "MAP-A" })!;

        Assert.Contains("N1", result);
        Assert.Contains("E1", result);
        Assert.DoesNotContain("N2", result);
        Assert.DoesNotContain("E9", result);
    }

    private static AStarPathFinder CreatePathFinder()
    {
        return new AStarPathFinder(NullLogger<AStarPathFinder>.Instance);
    }

    private static PathRequest CreateSimpleRequest(PathGraph graph)
    {
        return new PathRequest
        {
            RobotId = Guid.NewGuid(),
            MapId = graph.MapId,
            StartNodeId = GetStartNode(graph),
            EndNodeId = GetEndNode(graph),
            CurrentTheta = 0,
            MovementType = MovementType.Differential
        };
    }

    private static PathGraph CreateSimpleGraph()
    {
        var start = Guid.NewGuid();
        var end = Guid.NewGuid();
        var edgeId = Guid.NewGuid();

        var startNode = new PathNode
        {
            NodeId = start,
            NodeCode = "N1",
            X = 0,
            Y = 0,
            Active = true,
            AllowRotate = true
        };

        var endNode = new PathNode
        {
            NodeId = end,
            NodeCode = "N2",
            X = 1,
            Y = 0,
            Active = true,
            AllowRotate = true
        };

        var edge = new PathEdge
        {
            EdgeId = edgeId,
            EdgeCode = "E1",
            FromNodeId = start,
            ToNodeId = end,
            FromNodeCode = "N1",
            ToNodeCode = "N2",
            Active = true,
            Cost = 1,
            Length = 1,
            OrientationRads = new List<double> { 0, Math.PI },
            StartTangentRad = 0,
            DirectionRad = 0
        };

        startNode.OutEdges.Add(edge);
        endNode.InEdges.Add(edge);

        var graph = new PathGraph
        {
            MapId = Guid.NewGuid(),
            MapCode = "MAP-A"
        };
        graph.Nodes[start] = startNode;
        graph.Nodes[end] = endNode;
        graph.Edges[edgeId] = edge;

        return graph;
    }

    private static Guid GetStartNode(PathGraph graph)
    {
        return graph.Nodes.Values.Single(node => node.NodeCode == "N1").NodeId;
    }

    private static Guid GetEndNode(PathGraph graph)
    {
        return graph.Nodes.Values.Single(node => node.NodeCode == "N2").NodeId;
    }
}