using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Text.RegularExpressions;
using NUnit.Framework;
using Unity.Pipeline.Console;
using Unity.Pipeline.Editor.Console;
using UnityEngine;
using UnityEngine.TestTools;
namespace Unity.Pipeline.Tests.Editor.Console
{
///
/// Tests for the Editor console ground-truth sampler: what it publishes, that seeding leaves the
/// user's console filtering exactly as it found it, and that it re-arms capture when the console
/// moves and the buffer's cursor does not.
///
class EditorConsoleGroundTruthTests
{
[TearDown]
public void TearDown()
{
// A failed watchdog test would otherwise leave capture dead for the rest of the run.
ConsoleLogCapture.EnsureCapturing();
}
[Test]
public void Sample_PublishesConsoleCountsAndCompilationFlag()
{
var marker = NewMarker("groundtruth");
EmitWarning(marker);
EditorConsoleGroundTruth.SampleForTests();
var truth = ConsoleLogCapture.GroundTruth;
Assert.IsNotNull(truth, "Sampling must publish a snapshot");
Assert.Greater(truth.ConsoleWarnings, 0, "The warning just logged is in the Editor console");
Assert.Less((DateTime.UtcNow - truth.SampledUtc).TotalSeconds, 30, "The snapshot should be the one just taken");
}
[Test]
public void Seed_RestoresConsoleFlagsAndFilterText()
{
if (!EditorConsoleEntries.Available)
Assert.Ignore("UnityEditor.LogEntries is not reachable on this Editor version");
var originalFlags = GetConsoleFlags();
var originalFilter = GetFilteringText();
try
{
// Restrict the console the way a user might: hide errors and set a search filter.
// Seeding has to force both open to see every entry, then put them back.
SetConsoleFlags(originalFlags & ~LogLevelError);
SetFilteringText("pipeline_seed_filter_probe");
var restrictedFlags = GetConsoleFlags();
var restrictedFilter = GetFilteringText();
EditorConsoleGroundTruth.ResetForTests();
EditorConsoleGroundTruth.SampleForTests();
Assert.AreEqual(restrictedFlags, GetConsoleFlags(), "Seeding must leave the console's level toggles as it found them");
Assert.AreEqual(restrictedFilter, GetFilteringText(), "Seeding must leave the console's search text as it found it");
}
finally
{
SetFilteringText(originalFilter);
SetConsoleFlags(originalFlags);
}
}
[Test]
public void Seed_DoesNotDuplicateEntriesAlreadyCaptured()
{
if (!EditorConsoleEntries.Available)
Assert.Ignore("UnityEditor.LogEntries is not reachable on this Editor version");
var marker = NewMarker("dedupe");
EmitError(marker);
Assert.AreEqual(1, CountCaptures(marker), "The live callback captured it once");
EditorConsoleGroundTruth.ResetForTests();
EditorConsoleGroundTruth.SampleForTests();
Assert.AreEqual(1, CountCaptures(marker), "Seeding must not re-add an entry the buffer already holds");
}
[Test]
public void Seed_RecoversEntriesLoggedWhileCaptureWasDead()
{
if (!EditorConsoleEntries.Available)
Assert.Ignore("UnityEditor.LogEntries is not reachable on this Editor version");
// The repair path, and the shape of the reported symptom: the Editor console holds an
// error the buffer never received, because capture was not listening when it was logged.
// Editor compile errors are the real case — they are sticky, so they stand in the console
// long after the callback missed them.
StripSubscription();
var marker = NewMarker("missed");
EmitError(marker);
Assert.AreEqual(0, CountCaptures(marker),
"Precondition: the dead subscription must have missed it");
ConsoleLogCapture.EnsureCapturing();
EditorConsoleGroundTruth.ResetForTests();
EditorConsoleGroundTruth.SampleForTests();
Assert.AreEqual(1, CountCaptures(marker),
"Seeding must recover the entry the console holds and the buffer missed");
var seeded = ConsoleLogCapture.Buffer.Query(-1, 0, ConsoleLogBuffer.SeverityLog)
.Entries.First(e => e.Message != null && e.Message.Contains(marker));
Assert.IsTrue(seeded.Seeded,
"A recovered entry must be marked, since its timestamp is the read time");
}
[Test]
public void Seed_RecoversARepeatedEntryTheBufferMissed()
{
if (!EditorConsoleEntries.Available)
Assert.Ignore("UnityEditor.LogEntries is not reachable on this Editor version");
// Console messages repeat by design, so matching on presence alone is not enough: the
// copy the buffer already holds would cancel out the identical copy it missed, and that
// one would never be recovered.
var marker = NewMarker("repeated");
EmitError(marker);
Assert.AreEqual(1, CountCaptures(marker), "The live callback captured the first one");
StripSubscription();
EmitError(marker);
Assert.AreEqual(1, CountCaptures(marker),
"Precondition: the second, identical entry was missed while capture was dead");
ConsoleLogCapture.EnsureCapturing();
EditorConsoleGroundTruth.ResetForTests();
EditorConsoleGroundTruth.SampleForTests();
Assert.AreEqual(2, CountCaptures(marker),
"Both occurrences must be held after seeding, not just one");
}
[Test]
public void Seed_RecoversEveryOccurrenceOfAPreCaptureRepeat()
{
if (!EditorConsoleEntries.Available)
Assert.Ignore("UnityEditor.LogEntries is not reachable on this Editor version");
// A fresh seed with nothing held: all three identical rows have to come across.
StripSubscription();
var marker = NewMarker("triple");
EmitError(marker);
EmitError(marker);
EmitError(marker);
Assert.AreEqual(0, CountCaptures(marker), "Precondition: capture was dead for all three");
ConsoleLogCapture.EnsureCapturing();
EditorConsoleGroundTruth.ResetForTests();
EditorConsoleGroundTruth.SampleForTests();
Assert.AreEqual(3, CountCaptures(marker), "Every occurrence must be recovered");
}
[Test]
public void Watchdog_ReArmsCaptureWhenConsoleMovedAndBufferDidNot()
{
StripSubscription();
// Baseline, then a log the dead subscription cannot deliver: the console's count moves
// while the buffer's cursor stands still, which is the only symptom available.
EditorConsoleGroundTruth.SampleForTests();
EmitError(NewMarker("gap"));
EditorConsoleGroundTruth.SampleForTests();
EditorConsoleGroundTruth.SampleForTests();
var marker = NewMarker("recovered");
EmitError(marker);
Assert.AreEqual(1, CountCaptures(marker), "The watchdog must have re-armed capture");
}
[Test]
public void EditorConsoleEntries_ReadsRecentEntries()
{
if (!EditorConsoleEntries.Available)
Assert.Ignore("UnityEditor.LogEntries is not reachable on this Editor version");
var marker = NewMarker("read");
EmitError(marker);
var entries = new List();
Assert.IsTrue(EditorConsoleEntries.TryReadRecent(200, entries));
Assert.IsTrue(entries.Any(e => e.Message != null && e.Message.Contains(marker)),
"The entry just logged must be readable from the Editor console store");
}
// ConsoleMode.kLogLevelError, as mirrored by the internal ConsoleWindow.ConsoleFlags.
const int LogLevelError = 1 << 9;
static readonly Type k_LogEntries = Type.GetType("UnityEditor.LogEntries,UnityEditor");
static int GetConsoleFlags() => (int)Property("consoleFlags").GetGetMethod(true).Invoke(null, null);
static void SetConsoleFlags(int flags) => Property("consoleFlags").GetSetMethod(true).Invoke(null, new object[] { flags });
static string GetFilteringText() => (string)Method("GetFilteringText").Invoke(null, null) ?? string.Empty;
static void SetFilteringText(string text) => Method("SetFilteringText").Invoke(null, new object[] { text });
static PropertyInfo Property(string name) =>
k_LogEntries.GetProperty(name, BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static);
static MethodInfo Method(string name) =>
k_LogEntries.GetMethod(name, BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static);
static string NewMarker(string tag) => $"groundtruth_{tag}_{Guid.NewGuid():N}";
static void EmitError(string marker)
{
LogAssert.Expect(LogType.Error, new Regex(".*" + Regex.Escape(marker) + ".*"));
Debug.LogError(marker);
}
static void EmitWarning(string marker)
{
LogAssert.Expect(LogType.Warning, new Regex(".*" + Regex.Escape(marker) + ".*"));
Debug.LogWarning(marker);
}
static void StripSubscription()
{
var method = typeof(ConsoleLogCapture).GetMethod("OnLogMessage", BindingFlags.NonPublic | BindingFlags.Static);
Assert.IsNotNull(method, "ConsoleLogCapture.OnLogMessage must exist for this test to strip its subscription");
var handler = (Application.LogCallback)Delegate.CreateDelegate(typeof(Application.LogCallback), method);
Application.logMessageReceivedThreaded -= handler;
}
static int CountCaptures(string marker) =>
ConsoleLogCapture.Buffer.Query(-1, 0, ConsoleLogBuffer.SeverityLog)
.Entries.Count(e => e.Message != null && e.Message.Contains(marker));
}
}