TaskDispatchBackgroundServiceTests.cs 6.98 KB
using System.Reflection;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using Rcs.Domain.Entities;
using Rcs.Domain.Settings;
using Rcs.Infrastructure.Services;
using Xunit;

namespace Rcs.Infrastructure.Tests;

public class TaskDispatchBackgroundServiceTests
{
    [Fact]
    public async Task MixedLineCheck_ReturnsSoftFailure_WhenActiveExclusiveTypeConflictsWithDispatchType()
    {
        var exclusiveBeginTypeId = Guid.NewGuid();
        var dispatchBeginTypeId = Guid.NewGuid();
        var activeTask = CreateTask("ACTIVE", priority: 20, exclusiveBeginTypeId);
        var dispatchTask = CreateTask("P1001", priority: 55, dispatchBeginTypeId);
        var dispatchBeginType = new StorageLocationType
        {
            TypeId = dispatchBeginTypeId,
            TypeCode = "NORMAL",
            TypeName = "Normal",
            IsMixedLineShelfTaskExclusive = false
        };

        var result = await InvokeMixedLineCheckAsync(
            dispatchTask,
            dispatchBeginType,
            new[] { activeTask },
            new Dictionary<Guid, bool>
            {
                [exclusiveBeginTypeId] = true,
                [dispatchBeginTypeId] = false
            });

        Assert.False(GetProperty<bool>(result, "CanAssign"));
        Assert.True(GetProperty<bool>(result, "IsMixedLineSoftConstraintFailure"));
        Assert.True(GetProperty<bool>(result, "HasActiveExclusiveLock"));
        Assert.Equal(exclusiveBeginTypeId, GetProperty<Guid?>(result, "ActiveExclusiveBeginTypeId"));
    }

    [Fact]
    public async Task MixedLineCheck_DoesNotReturnSoftFailure_WhenActiveTaskHasNoBeginLocationType()
    {
        var dispatchBeginTypeId = Guid.NewGuid();
        var activeTask = new RobotTask
        {
            TaskId = Guid.NewGuid(),
            TaskCode = "ACTIVE",
            Priority = 20,
            BeginLocation = new StorageLocation
            {
                LocationId = Guid.NewGuid(),
                LocationCode = "ACTIVE_BEGIN"
            }
        };
        var dispatchTask = CreateTask("P1001", priority: 55, dispatchBeginTypeId);
        var dispatchBeginType = new StorageLocationType
        {
            TypeId = dispatchBeginTypeId,
            TypeCode = "MIXED",
            TypeName = "Mixed",
            IsMixedLineShelfTaskExclusive = true
        };

        var result = await InvokeMixedLineCheckAsync(
            dispatchTask,
            dispatchBeginType,
            new[] { activeTask },
            new Dictionary<Guid, bool>());

        Assert.False(GetProperty<bool>(result, "CanAssign"));
        Assert.False(GetProperty<bool>(result, "IsMixedLineSoftConstraintFailure"));
    }

    [Fact]
    public void RoundState_SkipsOnlyTasksBelowTheSoftFailurePriority()
    {
        var serviceType = typeof(TaskDispatchBackgroundService);
        var stateType = serviceType.GetNestedType("DispatchRoundRobotState", BindingFlags.NonPublic)!;
        var statesType = typeof(Dictionary<,>).MakeGenericType(typeof(Guid), stateType);
        var states = Activator.CreateInstance(statesType)!;
        var robotId = Guid.NewGuid();

        serviceType.GetMethod("ExcludeRobotForSoftMixedLineFailure", BindingFlags.NonPublic | BindingFlags.Static)!
            .Invoke(null, new object[] { robotId, 55, states });

        var shouldSkip = serviceType.GetMethod(
            "ShouldSkipRobotExcludedByHigherPrioritySoftMixedLineFailure",
            BindingFlags.NonPublic | BindingFlags.Static)!;

        var lowerPriorityTask = new RobotTask { TaskId = Guid.NewGuid(), TaskCode = "LOW", Priority = 20 };
        var samePriorityTask = new RobotTask { TaskId = Guid.NewGuid(), TaskCode = "SAME", Priority = 55 };

        Assert.True((bool)shouldSkip.Invoke(null, new object[] { lowerPriorityTask, robotId, states })!);
        Assert.False((bool)shouldSkip.Invoke(null, new object[] { samePriorityTask, robotId, states })!);
    }

    private static async Task<object> InvokeMixedLineCheckAsync(
        RobotTask dispatchTask,
        StorageLocationType dispatchBeginType,
        IReadOnlyCollection<RobotTask> activeTasks,
        Dictionary<Guid, bool> mixedLineExclusiveCache)
    {
        var service = new TaskDispatchBackgroundService(
            NullLogger<TaskDispatchBackgroundService>.Instance,
            new ServiceCollection().BuildServiceProvider(),
            agvPathService: null!,
            settingsMonitor: new StaticOptionsMonitor<AppSettings>(CreateTestAppSettings()));

        var method = typeof(TaskDispatchBackgroundService).GetMethod(
            "CanAssignMixedLineExclusiveTaskAsync",
            BindingFlags.Instance | BindingFlags.NonPublic)!;

        var invocation = (Task)method.Invoke(
            service,
            new object?[]
            {
                dispatchTask,
                dispatchBeginType,
                new Robot { RobotId = Guid.NewGuid(), RobotCode = "001" },
                activeTasks,
                null,
                mixedLineExclusiveCache,
                CancellationToken.None
            })!;

        await invocation;

        return invocation.GetType().GetProperty("Result")!.GetValue(invocation)!;
    }

    private static RobotTask CreateTask(string taskCode, int priority, Guid beginTypeId)
    {
        return new RobotTask
        {
            TaskId = Guid.NewGuid(),
            TaskCode = taskCode,
            Priority = priority,
            BeginLocation = new StorageLocation
            {
                LocationId = Guid.NewGuid(),
                LocationCode = $"{taskCode}_BEGIN",
                MapNode = new MapNode
                {
                    NodeId = Guid.NewGuid(),
                    NodeCode = $"{taskCode}_NODE",
                    StorageLocationTypeId = beginTypeId
                }
            }
        };
    }

    private static T GetProperty<T>(object instance, string propertyName)
    {
        return (T)instance.GetType().GetProperty(propertyName)!.GetValue(instance)!;
    }

    private static AppSettings CreateTestAppSettings()
    {
        return new AppSettings
        {
            Redis = new Redis
            {
                Host = "localhost",
                Port = "6379",
                Password = string.Empty
            },
            RabbitMq = new RabbitMq(),
            ConnSql = new ConnSql("Host=localhost;Port=5432;Database=test;Username=test;Password=test"),
            Cache = new Cache(60),
            Cors = new Cors(),
            Mqtt = new Rcs.Domain.Settings.Mqtt(),
            RobotStatus = new RobotStatusSync(),
            LanYinSettings = new LanYinSettings(),
            WmsSettings = new WmsSettings()
        };
    }

    private sealed class StaticOptionsMonitor<T> : IOptionsMonitor<T>
    {
        public StaticOptionsMonitor(T value)
        {
            CurrentValue = value;
        }

        public T CurrentValue { get; }

        public T Get(string? name) => CurrentValue;

        public IDisposable? OnChange(Action<T, string?> listener) => null;
    }
}