using System.Collections.Concurrent; using Serilog; using Serilog.Core; using Serilog.Events; using ZB.MOM.WW.OtOpcUa.Core.Abstractions; using ZB.MOM.WW.OtOpcUa.Core.Scripting; using ZB.MOM.WW.OtOpcUa.Core.Scripting.OutOfProcess; namespace ZB.MOM.WW.OtOpcUa.ScriptWorker.Tests; /// Collects Serilog events in memory. internal sealed class CapturingSink : ILogEventSink { public ConcurrentQueue Events { get; } = new(); public void Emit(LogEvent logEvent) => Events.Enqueue(logEvent); public ILogger CreateLogger() => new LoggerConfiguration().MinimumLevel.Verbose().WriteTo.Sink(this).CreateLogger(); public IEnumerable Rendered => Events.Select(e => e.RenderMessage()); } internal static class Scripts { public static readonly IReadOnlyDictionary NoInputs = new Dictionary(StringComparer.Ordinal); public static IReadOnlyDictionary Input(string path, object? value, uint status = 0) => new Dictionary(StringComparer.Ordinal) { [path] = new DataValueSnapshot(value, status, DateTime.UtcNow, DateTime.UtcNow), }; public static ScriptInvocation Vt(string key, string source, TimeSpan? timeout = null, IReadOnlyDictionary? inputs = null, ILogger? logger = null) => new(key, ScriptContextKind.VirtualTag, source, inputs ?? NoInputs, timeout ?? TimeSpan.FromSeconds(5), logger); /// Options for a single-shard runner with short budgets, so the containment tests run in seconds. public static ScriptingOptions Options(int shards = 1, TimeSpan? hardGrace = null, int quarantineAfterKills = 2, int probation = 20, TimeSpan? unavailableGrace = null, string? workerPath = null, long heapLimit = 256L * 1024 * 1024) => new() { Runner = ScriptRunnerMode.OutOfProcess, QuarantineAfterConsecutiveTimeouts = 100, Worker = new ScriptWorkerOptions { Shards = shards, HardGrace = hardGrace ?? TimeSpan.FromMilliseconds(500), QuarantineAfterKills = quarantineAfterKills, IsolationProbationRuns = probation, UnavailableGrace = unavailableGrace ?? TimeSpan.FromSeconds(30), ExecutablePath = workerPath, HeapLimitBytes = heapLimit, }, }; public static OutOfProcessScriptRunner Runner(ScriptingOptions options, ILogger? log = null) => new(options, log ?? new LoggerConfiguration().CreateLogger(), null, heartbeatInterval: TimeSpan.FromSeconds(1), loadTimeout: TimeSpan.FromSeconds(60)); public static async Task Eventually(Func probe, TimeSpan timeout, string what) where T : class { var deadline = DateTime.UtcNow + timeout; while (DateTime.UtcNow < deadline) { if (probe() is { } value) return value; await Task.Delay(20); } throw new TimeoutException($"timed out waiting for {what}"); } public static async Task Eventually(Func probe, TimeSpan timeout, string what) { var deadline = DateTime.UtcNow + timeout; while (DateTime.UtcNow < deadline) { if (probe()) return; await Task.Delay(20); } throw new TimeoutException($"timed out waiting for {what}"); } }