namespace LibationUiBase.Tests;
[TestClass]
public class ExceptionDisplayTests
{
/// Outer exception with a fixed so golden-string tests are stable.
private sealed class ExceptionWithFixedStack : Exception
{
private string _fixedStack { get; }
public ExceptionWithFixedStack(string message, Exception? innerException, string fixedStack)
: base(message, innerException) => _fixedStack = fixedStack;
public override string? StackTrace => _fixedStack;
}
///
/// Golden output for when there are fourteen
/// links (fifteen messages: outer L0 … deepest L14): first ten inners,
/// two omitted (L11–L12), then the two deepest (L13–L14), then the outer stack trace.
///
[TestMethod]
public void _FullText_14Levels()
{
const string stack = "<>";
var messages = Enumerable.Range(0, 15).Select(i => $"L{i}").ToArray();
var ex = CreateChainWithFixedStack(stack, messages);
const string expected = """
L0
Inner exception: L1
Inner exception: L2
Inner exception: L3
Inner exception: L4
Inner exception: L5
Inner exception: L6
Inner exception: L7
Inner exception: L8
Inner exception: L9
Inner exception: L10
2 inner exceptions omitted.
Inner exception: L13
Inner exception: L14
<>
""";
Assert.AreEqual(expected.ReplaceLineEndings("\n"), Format(ex));
}
///
/// Same chain shape as , but the outer exception reports from .
private static Exception CreateChainWithFixedStack(string stackTrace, params string[] messages)
{
if (messages.Length == 0)
throw new ArgumentException("At least one message is required.", nameof(messages));
var innermost = new Exception(messages[^1]);
for (var i = messages.Length - 2; i >= 1; i--)
innermost = new Exception(messages[i], innermost);
return new ExceptionWithFixedStack(messages[0], innermost, stackTrace);
}
/// messages[0] = outer; each following string is the next message.
private static Exception CreateChain(params string[] messages)
{
if (messages.Length == 0)
throw new ArgumentException("At least one message is required.", nameof(messages));
var innermost = new Exception(messages[^1]);
for (var i = messages.Length - 2; i >= 0; i--)
innermost = new Exception(messages[i], innermost);
return innermost;
}
private static string Format(Exception ex) => ExceptionDisplay.FormatMessageAndStackTrace(ex).ReplaceLineEndings("\n");
[TestMethod]
public void NoInnerException_ContainsOuterMessageAndStack()
{
var ex = new Exception("outer only");
var text = Format(ex);
Assert.StartsWith("outer only\n", text);
Assert.IsFalse(text.Contains("Inner exception:", StringComparison.Ordinal));
if (ex.StackTrace is not null)
Assert.IsTrue(text.Contains(ex.StackTrace, StringComparison.Ordinal));
}
[TestMethod]
public void OneInner_ShowsInnerLine()
{
var ex = CreateChain("outer", "inner-a");
var text = Format(ex);
var expectedHead = """
outer
Inner exception: inner-a
""".ReplaceLineEndings("\n");
Assert.AreEqual(expectedHead + ex.StackTrace, text);
}
[TestMethod]
public void TenInners_ShowsAllTen()
{
var messages = new[] { "m0", "m1", "m2", "m3", "m4", "m5", "m6", "m7", "m8", "m9", "m10" };
var ex = CreateChain(messages);
var text = Format(ex);
for (var i = 1; i <= 10; i++)
Assert.IsTrue(text.Contains($"Inner exception: m{i}\n", StringComparison.Ordinal), $"missing inner m{i}");
Assert.IsFalse(text.Contains("omitted", StringComparison.OrdinalIgnoreCase));
}
[TestMethod]
public void ElevenInners_FirstTenThenDeepestOnly_NoOmitLine()
{
var messages = Enumerable.Range(0, 12).Select(i => $"n{i}").ToArray();
var ex = CreateChain(messages);
var text = Format(ex);
for (var i = 1; i <= 10; i++)
Assert.IsTrue(text.Contains($"Inner exception: n{i}\n", StringComparison.Ordinal));
Assert.IsFalse(text.Contains("omitted", StringComparison.OrdinalIgnoreCase));
Assert.IsTrue(text.Contains("Inner exception: n11\n", StringComparison.Ordinal));
}
[TestMethod]
public void TwelveInners_FirstTenThenLastTwo_NoOmitLine()
{
var messages = Enumerable.Range(0, 13).Select(i => $"p{i}").ToArray();
var ex = CreateChain(messages);
var text = Format(ex);
for (var i = 1; i <= 10; i++)
Assert.IsTrue(text.Contains($"Inner exception: p{i}\n", StringComparison.Ordinal));
Assert.IsFalse(text.Contains("omitted", StringComparison.OrdinalIgnoreCase));
Assert.IsTrue(text.Contains("Inner exception: p11\n", StringComparison.Ordinal));
Assert.IsTrue(text.Contains("Inner exception: p12\n", StringComparison.Ordinal));
}
[TestMethod]
public void ThirteenInners_FirstTen_OmitOne_ThenLastTwo()
{
var messages = Enumerable.Range(0, 14).Select(i => $"q{i}").ToArray();
var ex = CreateChain(messages);
var text = Format(ex);
for (var i = 1; i <= 10; i++)
Assert.IsTrue(text.Contains($"Inner exception: q{i}\n", StringComparison.Ordinal));
Assert.IsTrue(text.Contains("1 inner exception omitted.\n", StringComparison.Ordinal));
Assert.IsFalse(text.Contains("Inner exception: q11\n", StringComparison.Ordinal), "omitted inner should not appear");
Assert.IsTrue(text.Contains("Inner exception: q12\n", StringComparison.Ordinal));
Assert.IsTrue(text.Contains("Inner exception: q13\n", StringComparison.Ordinal));
}
[TestMethod]
public void ManyInners_OmitCountPlural()
{
var depth = 20;
var messages = Enumerable.Range(0, depth + 1).Select(i => $"x{i}").ToArray();
var ex = CreateChain(messages);
var text = Format(ex);
var omitted = depth - 10 - 2;
Assert.IsTrue(text.Contains($"{omitted} inner exceptions omitted.\n", StringComparison.Ordinal));
Assert.IsTrue(text.Contains($"Inner exception: x{depth - 1}\n", StringComparison.Ordinal));
Assert.IsTrue(text.Contains($"Inner exception: x{depth}\n", StringComparison.Ordinal));
}
}