using System; using System.Collections; using System.Collections.Generic; using System.Linq; using System.Reflection; using NUnit.Framework.Internal; using NUnit.Framework.Internal.Commands; using UnityEngine.TestTools; using UnityEngine.TestTools.Logging; namespace UnityEngine.TestRunner.NUnitExtensions.Runner { internal class UnityLogCheckDelegatingCommand : DelegatingTestCommand, IEnumerableTestMethodCommand { private static Dictionary s_AttributeCache = new Dictionary(); public UnityLogCheckDelegatingCommand(TestCommand innerCommand) : base(innerCommand) {} public override TestResult Execute(ITestExecutionContext context) { using (var logScope = new LogScope()) { if (ExecuteAndCheckLog(logScope, context.CurrentResult, () => innerCommand.Execute(context))) PostTestValidation(logScope, innerCommand, context.CurrentResult); } return context.CurrentResult; } public IEnumerable ExecuteEnumerable(ITestExecutionContext context) { if (!(innerCommand is IEnumerableTestMethodCommand enumerableTestMethodCommand)) { Execute(context); yield break; } using (var logScope = new LogScope()) { IEnumerable executeEnumerable = null; if (!ExecuteAndCheckLog(logScope, context.CurrentResult, () => executeEnumerable = enumerableTestMethodCommand.ExecuteEnumerable(context))) yield break; var innerCommandIsTask = enumerableTestMethodCommand is TaskTestMethodCommand; foreach (var step in executeEnumerable) { // do not check expected logs here - we want to permit expecting and receiving messages to run // across frames. This means that we break on failing logs and fail on next frame. // An exception is async (Task), in which case we first fail after the task has run, as we cannot cancel the task. if (!innerCommandIsTask && !CheckFailingLogs(logScope, context.CurrentResult)) { yield break; } yield return step; } if (!CheckLogs(context.CurrentResult, logScope)) yield break; PostTestValidation(logScope, innerCommand, context.CurrentResult); } } private static bool CaptureException(TestResult result, Action action) { try { action(); return true; } catch (Exception e) { result.RecordException(e); return false; } } private static bool ExecuteAndCheckLog(LogScope logScope, TestResult result, Action action) => CaptureException(result, action) && CheckLogs(result, logScope); private static void PostTestValidation(LogScope logScope, TestCommand command, TestResult result) { if (MustExpect(command.Test.Method.MethodInfo)) CaptureException(result, logScope.NoUnexpectedReceived); } private static bool CheckLogs(TestResult result, LogScope logScope) { try { logScope.EvaluateLogScope(true); } catch (Exception e) { result.RecordException(e); return false; } return true; } private static bool CheckFailingLogs(LogScope logScope, TestResult result) { try { logScope.EvaluateLogScope(false); } catch (Exception e) { result.RecordException(e); return false; } return true; } private static bool MustExpect(MemberInfo method) { // method var methodAttr = method.GetCustomAttributes(true).FirstOrDefault(); if (methodAttr != null) return methodAttr.MustExpect; // fixture var fixture = method.DeclaringType; if (!s_AttributeCache.TryGetValue(fixture, out var mustExpect)) { var fixtureAttr = fixture.GetCustomAttributes(true).FirstOrDefault(); mustExpect = s_AttributeCache[fixture] = fixtureAttr?.MustExpect; } if (mustExpect != null) return mustExpect.Value; // assembly var assembly = fixture.Assembly; if (!s_AttributeCache.TryGetValue(assembly, out mustExpect)) { var assemblyAttr = assembly.GetCustomAttributes().FirstOrDefault(); mustExpect = s_AttributeCache[assembly] = assemblyAttr?.MustExpect; } return mustExpect == true; } } }