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); /// /// Name of the environment variable that points the suite at a different worker entry than the one built beside /// the tests — e.g. the apphost of a self-contained script-worker/ from a -r win-x64 Host publish. /// public const string WorkerUnderTestVariable = "OTOPCUA_SCRIPTWORKER_TEST_PATH"; /// The worker entry named by , or null for the built one. public static string? WorkerUnderTest => Environment.GetEnvironmentVariable(WorkerUnderTestVariable) is { Length: > 0 } path ? path : null; /// 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, string? peCacheDir = null, TimeSpan? isolationIdle = null, TimeSpan? circuitOpenAfter = null) => 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 ?? WorkerUnderTest, HeapLimitBytes = heapLimit, PeCacheDir = peCacheDir, IsolationIdleTimeout = isolationIdle ?? ScriptWorkerOptions.DefaultIsolationIdleTimeout, CircuitOpenAfter = circuitOpenAfter ?? ScriptWorkerOptions.DefaultCircuitOpenAfter, }, }; /// A fresh, empty temp directory, deleted by . public sealed class TempDir : IDisposable { public TempDir() { Path = System.IO.Path.Combine(System.IO.Path.GetTempPath(), "otopcua-sw-test-" + Guid.NewGuid().ToString("N")[..12]); Directory.CreateDirectory(Path); } public string Path { get; } public void Dispose() { try { Directory.Delete(Path, recursive: true); } catch { /* best effort */ } } } public static OutOfProcessScriptRunner Runner(ScriptingOptions options, ILogger? log = null, TimeSpan? maintenanceInterval = null, string? markerDirectory = null) => new(options, log ?? new LoggerConfiguration().CreateLogger(), null, heartbeatInterval: TimeSpan.FromSeconds(1), loadTimeout: TimeSpan.FromSeconds(60), maintenanceInterval: maintenanceInterval, markerDirectory: markerDirectory); 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}"); } }