311 lines
12 KiB
C#
311 lines
12 KiB
C#
using System;
|
|
using System.IO;
|
|
using System.Linq;
|
|
using System.Threading.Tasks;
|
|
using Newtonsoft.Json;
|
|
using NUnit.Framework;
|
|
using Unity.Pipeline.Console;
|
|
using Unity.Pipeline.Models;
|
|
using UnityEngine;
|
|
|
|
namespace Unity.Pipeline.Tests.Editor
|
|
{
|
|
/// <summary>
|
|
/// Tests for <see cref="ConsoleLogBuffer"/> — the ring buffer behind the console command.
|
|
/// Exercised directly (no Unity log capture) for determinism.
|
|
/// </summary>
|
|
public class ConsoleLogBufferTests
|
|
{
|
|
static readonly DateTime k_Ts = new DateTime(2026, 1, 1, 0, 0, 0, DateTimeKind.Utc);
|
|
|
|
static ConsoleLogBuffer NewBuffer() => new ConsoleLogBuffer();
|
|
|
|
[Test]
|
|
public void Add_AssignsIncreasingSequenceNumbers()
|
|
{
|
|
var buffer = NewBuffer();
|
|
buffer.Add(LogType.Log, "a", null, k_Ts);
|
|
buffer.Add(LogType.Log, "b", null, k_Ts);
|
|
buffer.Add(LogType.Log, "c", null, k_Ts);
|
|
|
|
var entries = buffer.Query(-1, 100, ConsoleLogBuffer.SeverityLog).Entries;
|
|
|
|
CollectionAssert.AreEqual(new[] { "a", "b", "c" }, entries.Select(e => e.Message));
|
|
CollectionAssert.AreEqual(new long[] { 1, 2, 3 }, entries.Select(e => e.Seq));
|
|
}
|
|
|
|
[Test]
|
|
public void Query_Snapshot_ReturnsOldestFirst()
|
|
{
|
|
var buffer = NewBuffer();
|
|
buffer.Add(LogType.Log, "first", null, k_Ts);
|
|
buffer.Add(LogType.Log, "second", null, k_Ts);
|
|
|
|
var response = buffer.Query(-1, 100, ConsoleLogBuffer.SeverityLog);
|
|
|
|
Assert.AreEqual(2, response.Returned);
|
|
Assert.AreEqual("first", response.Entries[0].Message);
|
|
Assert.AreEqual("second", response.Entries[1].Message);
|
|
Assert.AreEqual(2, response.Cursor);
|
|
Assert.IsFalse(response.Dropped);
|
|
}
|
|
|
|
[Test]
|
|
public void Query_LevelThreshold_IsMinimumSeverity()
|
|
{
|
|
var buffer = NewBuffer();
|
|
buffer.Add(LogType.Log, "log", null, k_Ts);
|
|
buffer.Add(LogType.Warning, "warn", null, k_Ts);
|
|
buffer.Add(LogType.Error, "error", null, k_Ts);
|
|
buffer.Add(LogType.Exception, "exception", null, k_Ts);
|
|
|
|
var warnAndUp = buffer.Query(-1, 100, ConsoleLogBuffer.SeverityWarn).Entries;
|
|
CollectionAssert.AreEqual(new[] { "warn", "error", "exception" }, warnAndUp.Select(e => e.Message));
|
|
|
|
var errorOnly = buffer.Query(-1, 100, ConsoleLogBuffer.SeverityError).Entries;
|
|
CollectionAssert.AreEqual(new[] { "error", "exception" }, errorOnly.Select(e => e.Message));
|
|
|
|
var all = buffer.Query(-1, 100, ConsoleLogBuffer.SeverityLog).Entries;
|
|
Assert.AreEqual(4, all.Length);
|
|
}
|
|
|
|
[Test]
|
|
public void Query_Tail_ReturnsMostRecentMatches()
|
|
{
|
|
var buffer = NewBuffer();
|
|
for (int i = 0; i < 10; i++)
|
|
buffer.Add(LogType.Log, $"m{i}", null, k_Ts);
|
|
|
|
var entries = buffer.Query(-1, 3, ConsoleLogBuffer.SeverityLog).Entries;
|
|
|
|
CollectionAssert.AreEqual(new[] { "m7", "m8", "m9" }, entries.Select(e => e.Message));
|
|
}
|
|
|
|
[Test]
|
|
public void Query_Tail_AppliesAfterLevelFilter()
|
|
{
|
|
var buffer = NewBuffer();
|
|
buffer.Add(LogType.Error, "e0", null, k_Ts);
|
|
buffer.Add(LogType.Log, "l0", null, k_Ts);
|
|
buffer.Add(LogType.Error, "e1", null, k_Ts);
|
|
buffer.Add(LogType.Log, "l1", null, k_Ts);
|
|
buffer.Add(LogType.Error, "e2", null, k_Ts);
|
|
|
|
// tail=2 over error-only matches => last two errors, not last two of all entries.
|
|
var entries = buffer.Query(-1, 2, ConsoleLogBuffer.SeverityError).Entries;
|
|
|
|
CollectionAssert.AreEqual(new[] { "e1", "e2" }, entries.Select(e => e.Message));
|
|
}
|
|
|
|
[Test]
|
|
public void Query_Since_ReturnsOnlyNewerEntries()
|
|
{
|
|
var buffer = NewBuffer();
|
|
buffer.Add(LogType.Log, "a", null, k_Ts); // seq 1
|
|
buffer.Add(LogType.Log, "b", null, k_Ts); // seq 2
|
|
buffer.Add(LogType.Log, "c", null, k_Ts); // seq 3
|
|
|
|
var response = buffer.Query(2, 100, ConsoleLogBuffer.SeverityLog);
|
|
|
|
CollectionAssert.AreEqual(new[] { "c" }, response.Entries.Select(e => e.Message));
|
|
Assert.AreEqual(3, response.Cursor);
|
|
Assert.IsFalse(response.Dropped);
|
|
}
|
|
|
|
[Test]
|
|
public void Query_SinceAtCursor_ReturnsEmptyButReportsCursor()
|
|
{
|
|
var buffer = NewBuffer();
|
|
buffer.Add(LogType.Log, "a", null, k_Ts);
|
|
buffer.Add(LogType.Log, "b", null, k_Ts);
|
|
|
|
var response = buffer.Query(2, 100, ConsoleLogBuffer.SeverityLog);
|
|
|
|
Assert.AreEqual(0, response.Returned);
|
|
Assert.AreEqual(2, response.Cursor, "Cursor must still advance so a follow client resumes correctly");
|
|
Assert.IsFalse(response.Dropped);
|
|
}
|
|
|
|
[Test]
|
|
public void Query_SinceBeyondCursor_DoesNotOverflowDroppedCheck()
|
|
{
|
|
var buffer = NewBuffer();
|
|
buffer.Add(LogType.Log, "a", null, k_Ts);
|
|
|
|
var response = buffer.Query(long.MaxValue, 100, ConsoleLogBuffer.SeverityLog);
|
|
|
|
Assert.AreEqual(0, response.Returned);
|
|
Assert.IsFalse(response.Dropped);
|
|
}
|
|
|
|
[Test]
|
|
public void Query_CursorReflectsLatestEvenWhenFilteredOut()
|
|
{
|
|
var buffer = NewBuffer();
|
|
buffer.Add(LogType.Error, "boom", null, k_Ts); // seq 1
|
|
buffer.Add(LogType.Log, "noise", null, k_Ts); // seq 2, filtered out by error level
|
|
|
|
var response = buffer.Query(1, 100, ConsoleLogBuffer.SeverityError);
|
|
|
|
Assert.AreEqual(0, response.Returned, "The only new entry is below the level threshold");
|
|
Assert.AreEqual(2, response.Cursor, "Cursor advances past the filtered entry to avoid re-scanning it");
|
|
}
|
|
|
|
[Test]
|
|
public void Eviction_DropsOldestAndFlagsDropped()
|
|
{
|
|
var buffer = NewBuffer();
|
|
int total = ConsoleLogBuffer.Capacity + 50;
|
|
for (int i = 0; i < total; i++)
|
|
buffer.Add(LogType.Log, $"m{i}", null, k_Ts);
|
|
|
|
Assert.AreEqual(ConsoleLogBuffer.Capacity, buffer.Count, "Buffer should be capped at Capacity");
|
|
|
|
// since=1 points before the evicted window, so the consumer missed entries.
|
|
var response = buffer.Query(1, ConsoleLogBuffer.Capacity, ConsoleLogBuffer.SeverityLog);
|
|
Assert.IsTrue(response.Dropped, "Querying from an evicted cursor should set Dropped");
|
|
|
|
// The most recent entry is always present.
|
|
var latest = buffer.Query(-1, 1, ConsoleLogBuffer.SeverityLog).Entries.Single();
|
|
Assert.AreEqual($"m{total - 1}", latest.Message);
|
|
}
|
|
|
|
[Test]
|
|
public void Dropped_FalseWhenSinceWithinRetainedWindow()
|
|
{
|
|
var buffer = NewBuffer();
|
|
for (int i = 0; i < 10; i++)
|
|
buffer.Add(LogType.Log, $"m{i}", null, k_Ts);
|
|
|
|
var response = buffer.Query(5, 100, ConsoleLogBuffer.SeverityLog);
|
|
Assert.IsFalse(response.Dropped);
|
|
}
|
|
|
|
[Test]
|
|
public void SaveAndLoad_RoundTripsEntriesAndCursor()
|
|
{
|
|
var path = Path.Combine("Temp", $"pipeline_console_test_{Guid.NewGuid():N}.json");
|
|
try
|
|
{
|
|
var original = NewBuffer();
|
|
original.Add(LogType.Log, "log", null, k_Ts);
|
|
original.Add(LogType.Warning, "warn", "trace", k_Ts);
|
|
original.Add(LogType.Error, "error", null, k_Ts);
|
|
original.Save(path);
|
|
|
|
var restored = NewBuffer();
|
|
Assert.IsTrue(restored.Load(path));
|
|
|
|
var entries = restored.Query(-1, 100, ConsoleLogBuffer.SeverityLog).Entries;
|
|
CollectionAssert.AreEqual(new[] { "log", "warn", "error" }, entries.Select(e => e.Message));
|
|
Assert.AreEqual(ConsoleLogBuffer.LevelWarn, entries[1].Level, "Severity must survive the round trip");
|
|
Assert.AreEqual("trace", entries[1].StackTrace);
|
|
|
|
// Sequence continues monotonically after a restore.
|
|
restored.Add(LogType.Log, "after", null, k_Ts);
|
|
Assert.AreEqual(4, restored.Query(3, 100, ConsoleLogBuffer.SeverityLog).Entries.Single().Seq);
|
|
}
|
|
finally
|
|
{
|
|
if (File.Exists(path)) File.Delete(path);
|
|
}
|
|
}
|
|
|
|
[Test]
|
|
public void Load_MissingFile_ReturnsFalseAndLeavesBufferEmpty()
|
|
{
|
|
var buffer = NewBuffer();
|
|
Assert.IsFalse(buffer.Load(Path.Combine("Temp", $"does_not_exist_{Guid.NewGuid():N}.json")));
|
|
Assert.AreEqual(0, buffer.Count);
|
|
}
|
|
|
|
[Test]
|
|
public void Load_ClampsRestoredEntriesToCapacityKeepingMostRecent()
|
|
{
|
|
var path = Path.Combine("Temp", $"pipeline_console_oversized_{Guid.NewGuid():N}.json");
|
|
try
|
|
{
|
|
Directory.CreateDirectory(Path.GetDirectoryName(path) ?? "Temp");
|
|
|
|
var entries = Enumerable.Range(1, ConsoleLogBuffer.Capacity + 5)
|
|
.Select(i => new ConsoleLogEntry
|
|
{
|
|
Seq = i,
|
|
TimestampUtc = k_Ts,
|
|
Level = ConsoleLogBuffer.LevelLog,
|
|
Message = $"m{i}",
|
|
StackTrace = string.Empty
|
|
})
|
|
.ToArray();
|
|
|
|
File.WriteAllText(path, JsonConvert.SerializeObject(new
|
|
{
|
|
lastSeq = (long)(ConsoleLogBuffer.Capacity + 5),
|
|
entries
|
|
}));
|
|
|
|
var buffer = NewBuffer();
|
|
Assert.IsTrue(buffer.Load(path));
|
|
Assert.AreEqual(ConsoleLogBuffer.Capacity, buffer.Count);
|
|
|
|
var restored = buffer.Query(-1, ConsoleLogBuffer.Capacity, ConsoleLogBuffer.SeverityLog).Entries;
|
|
Assert.AreEqual("m6", restored.First().Message);
|
|
Assert.AreEqual($"m{ConsoleLogBuffer.Capacity + 5}", restored.Last().Message);
|
|
}
|
|
finally
|
|
{
|
|
if (File.Exists(path)) File.Delete(path);
|
|
}
|
|
}
|
|
|
|
[Test]
|
|
public void Clear_EmptiesEntriesButKeepsSequenceMonotonic()
|
|
{
|
|
var buffer = NewBuffer();
|
|
buffer.Add(LogType.Log, "a", null, k_Ts);
|
|
buffer.Add(LogType.Log, "b", null, k_Ts);
|
|
|
|
buffer.Clear();
|
|
Assert.AreEqual(0, buffer.Count);
|
|
|
|
buffer.Add(LogType.Log, "c", null, k_Ts);
|
|
Assert.AreEqual(3, buffer.LastSeq, "Sequence must not reset on Clear");
|
|
}
|
|
|
|
[Test]
|
|
public void Add_IsThreadSafe()
|
|
{
|
|
var buffer = NewBuffer();
|
|
const int threads = 8;
|
|
const int perThread = 500;
|
|
|
|
var tasks = Enumerable.Range(0, threads).Select(t => Task.Run(() =>
|
|
{
|
|
for (int i = 0; i < perThread; i++)
|
|
buffer.Add(LogType.Log, $"t{t}-{i}", null, k_Ts);
|
|
})).ToArray();
|
|
Task.WaitAll(tasks);
|
|
|
|
Assert.AreEqual(threads * perThread, buffer.LastSeq, "Every add must get a unique sequence number");
|
|
Assert.AreEqual(ConsoleLogBuffer.Capacity, buffer.Count);
|
|
|
|
// All retained seqs are unique.
|
|
var seqs = buffer.Query(-1, ConsoleLogBuffer.Capacity, ConsoleLogBuffer.SeverityLog)
|
|
.Entries.Select(e => e.Seq).ToArray();
|
|
Assert.AreEqual(seqs.Length, seqs.Distinct().Count(), "Sequence numbers must be unique");
|
|
}
|
|
|
|
[Test]
|
|
public void SeverityFromLevelName_ParsesNamesAndAliases()
|
|
{
|
|
Assert.AreEqual(ConsoleLogBuffer.SeverityError, ConsoleLogBuffer.SeverityFromLevelName("error"));
|
|
Assert.AreEqual(ConsoleLogBuffer.SeverityError, ConsoleLogBuffer.SeverityFromLevelName("ERR"));
|
|
Assert.AreEqual(ConsoleLogBuffer.SeverityWarn, ConsoleLogBuffer.SeverityFromLevelName("warn"));
|
|
Assert.AreEqual(ConsoleLogBuffer.SeverityWarn, ConsoleLogBuffer.SeverityFromLevelName("Warning"));
|
|
Assert.AreEqual(ConsoleLogBuffer.SeverityLog, ConsoleLogBuffer.SeverityFromLevelName("log"));
|
|
Assert.AreEqual(ConsoleLogBuffer.SeverityLog, ConsoleLogBuffer.SeverityFromLevelName("nonsense"));
|
|
Assert.AreEqual(ConsoleLogBuffer.SeverityLog, ConsoleLogBuffer.SeverityFromLevelName(null));
|
|
}
|
|
}
|
|
}
|