using System; using System.Collections.Generic; using System.IO; using NUnit.Framework; using Robust.Shared.Log; using Robust.Shared.Timing; using Serilog.Events; namespace Robust.UnitTesting.Pool; /// /// Log handler intended for pooled integration tests. /// /// /// /// This class logs to one place: an NUnit (so it nicely gets attributed to a test in your IDE) /// /// /// The active test context can be swapped out so pooled instances can correctly have their logs attributed. /// /// public sealed class PoolTestLogHandler : ILogHandler { private readonly string? _prefix; private RStopwatch _stopwatch; public TextWriter? ActiveContext { get; private set; } public LogLevel? FailureLevel { get; set; } public IReadOnlyList FailingLogs => _failingLogs; private readonly List _failingLogs = new(); public PoolTestLogHandler(string? prefix) { _prefix = prefix != null ? $"{prefix}: " : ""; } public bool ShuttingDown; /// /// /// Event handler that allows you to override a potential failing log. /// Use this if you want to allow certain error logs to be considered passing. /// /// /// Has the sawmill name and passed in, and should return a boolean /// when the log message should not be logged as a failure. /// /// public event Func? JudgeLog; public void Log(string sawmillName, LogEvent message) { var level = message.Level.ToRobust(); if (ShuttingDown && (FailureLevel == null || level < FailureLevel)) return; if (ActiveContext is not { } testContext) { // If this gets hit it means something is logging to this instance while it's "between" tests. // This is a bug in either the game or the testing system, and must always be investigated. throw new InvalidOperationException("Log to pool test log handler without active test context"); } var name = LogMessage.LogLevelToName(level); var seconds = _stopwatch.Elapsed.TotalSeconds; var rendered = message.RenderMessage(); var line = $"{_prefix}{seconds:F3}s [{name}] {sawmillName}: {rendered}"; testContext.WriteLine(line); if (FailureLevel == null || level < FailureLevel || (JudgeLog?.Invoke(sawmillName, message) ?? false)) return; testContext.Flush(); _failingLogs.Add($"{line} Exception: {message.Exception}"); } public void ClearContext() { ActiveContext = null; _failingLogs.Clear(); } public void ActivateContext(TextWriter context) { _stopwatch.Restart(); ActiveContext = context; } }