120 lines
5.6 KiB
C#
120 lines
5.6 KiB
C#
using Akka.Actor;
|
|
using Akka.TestKit.Xunit2;
|
|
using Microsoft.Extensions.DependencyInjection;
|
|
using Microsoft.Extensions.Logging;
|
|
using Microsoft.Extensions.Logging.Abstractions;
|
|
using NSubstitute;
|
|
using ZB.MOM.WW.ScadaBridge.Commons.Entities.InboundApi;
|
|
using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Repositories;
|
|
using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Services;
|
|
using ZB.MOM.WW.ScadaBridge.Commons.Messages.Management;
|
|
using ZB.MOM.WW.ScadaBridge.InboundAPI;
|
|
|
|
namespace ZB.MOM.WW.ScadaBridge.ManagementService.Tests;
|
|
|
|
/// <summary>
|
|
/// Review 2026-10-21 05/M1: an API-method save used to compile the script twice (a
|
|
/// <c>ValidateCompilation</c> pre-pass, then <c>CompileAndRegister</c>), and each compile
|
|
/// pins a non-collectible assembly. A save must compile exactly once — including when the
|
|
/// registration is deferred until after the configuration transaction commits.
|
|
/// </summary>
|
|
public class ApiMethodSingleCompileTests : TestKit
|
|
{
|
|
private readonly ServiceCollection _services = new ServiceCollection().AddTestConfigurationTransaction();
|
|
private readonly IInboundApiRepository _apiRepo = Substitute.For<IInboundApiRepository>();
|
|
private readonly CompileCountingLogger _logger = new();
|
|
private readonly CollectingCommitActions _commitActions = new();
|
|
|
|
/// <summary>Initializes the harness with a real executor whose compiles are counted.</summary>
|
|
public ApiMethodSingleCompileTests()
|
|
{
|
|
_services.AddScoped(_ => _apiRepo);
|
|
_services.AddScoped(_ => Substitute.For<IAuditService>());
|
|
_services.AddSingleton(new InboundScriptExecutor(_logger, Substitute.For<IServiceProvider>()));
|
|
_apiRepo.SaveChangesAsync(Arg.Any<CancellationToken>()).Returns(1);
|
|
}
|
|
|
|
private IActorRef CreateActor() =>
|
|
Sys.ActorOf(Props.Create(() => new ManagementActor(
|
|
_services.BuildServiceProvider(), NullLogger<ManagementActor>.Instance)));
|
|
|
|
private static ManagementEnvelope Envelope(object command) =>
|
|
new(new AuthenticatedUser("designer", "Designer", new[] { "Designer" }, Array.Empty<string>()),
|
|
command, Guid.NewGuid().ToString("N"));
|
|
|
|
/// <summary>A compiling create without deferred commit actions compiles once.</summary>
|
|
[Fact]
|
|
public void Create_CompilingScript_CompilesOnce()
|
|
{
|
|
CreateActor().Tell(Envelope(new CreateApiMethodCommand("Once", "return 1;", 30, null, null)));
|
|
|
|
var response = ExpectMsg<ManagementSuccess>(TimeSpan.FromSeconds(30));
|
|
Assert.Equal(1, _logger.CompileCount);
|
|
Assert.DoesNotContain("failed to compile", response.JsonData);
|
|
}
|
|
|
|
/// <summary>
|
|
/// With the registration deferred to post-commit, the warning is still derived before the
|
|
/// reply and the queued publish reuses the compiled result instead of recompiling.
|
|
/// </summary>
|
|
/// <returns>A task that represents the asynchronous operation.</returns>
|
|
[Fact]
|
|
public async Task Update_DeferredRegistration_CompilesOnce_AndStillWarns()
|
|
{
|
|
_services.AddScoped<IConfigurationCommitActions>(_ => _commitActions);
|
|
var existing = new ApiMethod("M", "return 1;") { Id = 7, TimeoutSeconds = 10 };
|
|
_apiRepo.GetApiMethodByIdAsync(7, Arg.Any<CancellationToken>()).Returns(existing);
|
|
|
|
CreateActor().Tell(Envelope(new UpdateApiMethodCommand(7, "return doesNotExist;", 10, null, null)));
|
|
|
|
var response = ExpectMsg<ManagementSuccess>(TimeSpan.FromSeconds(30));
|
|
Assert.Contains("failed to compile", response.JsonData);
|
|
foreach (var action in _commitActions.Actions)
|
|
await action();
|
|
Assert.NotEmpty(_commitActions.Actions);
|
|
Assert.Equal(0, _logger.CompileCount); // the broken script never reaches "compiled"
|
|
Assert.Equal(1, _logger.CompileFailureCount);
|
|
}
|
|
|
|
/// <summary>A compiling update with deferred registration compiles once across reply + publish.</summary>
|
|
/// <returns>A task that represents the asynchronous operation.</returns>
|
|
[Fact]
|
|
public async Task Update_DeferredRegistration_CompilingScript_CompilesOnce()
|
|
{
|
|
_services.AddScoped<IConfigurationCommitActions>(_ => _commitActions);
|
|
var existing = new ApiMethod("M2", "return 1;") { Id = 8, TimeoutSeconds = 10 };
|
|
_apiRepo.GetApiMethodByIdAsync(8, Arg.Any<CancellationToken>()).Returns(existing);
|
|
|
|
CreateActor().Tell(Envelope(new UpdateApiMethodCommand(8, "return 2;", 10, null, null)));
|
|
|
|
ExpectMsg<ManagementSuccess>(TimeSpan.FromSeconds(30));
|
|
foreach (var action in _commitActions.Actions)
|
|
await action();
|
|
Assert.Equal(1, _logger.CompileCount);
|
|
}
|
|
|
|
private sealed class CollectingCommitActions : IConfigurationCommitActions
|
|
{
|
|
public List<Func<Task>> Actions { get; } = new();
|
|
public void Add(Func<Task> action) => Actions.Add(action);
|
|
}
|
|
|
|
private sealed class CompileCountingLogger : ILogger<InboundScriptExecutor>
|
|
{
|
|
private int _count;
|
|
private int _failures;
|
|
public int CompileCount => _count;
|
|
public int CompileFailureCount => _failures;
|
|
public IDisposable? BeginScope<TState>(TState state) where TState : notnull => null;
|
|
public bool IsEnabled(LogLevel logLevel) => true;
|
|
public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func<TState, Exception?, string> formatter)
|
|
{
|
|
var text = formatter(state, exception);
|
|
if (text.EndsWith("script compiled", StringComparison.Ordinal))
|
|
Interlocked.Increment(ref _count);
|
|
else if (text.Contains("script compilation failed", StringComparison.Ordinal))
|
|
Interlocked.Increment(ref _failures);
|
|
}
|
|
}
|
|
}
|