From 3df28478612a2ed0d5df57ffc7e9efb427c24802 Mon Sep 17 00:00:00 2001 From: Chrison Simtian Date: Mon, 27 Jul 2026 01:41:59 +1200 Subject: [PATCH 1/2] Add specs pinning console level labels to the level names Render each level through the console output templates with the same formatter the console sink uses, and assert on the label. 19 of 21 fail: the templates use {Level:u3}, so lines are labelled DBG, INF, WRN and so on. Part of #570 Co-Authored-By: Claude Opus 5 (1M context) --- .../OutputTemplateLevelSpecs.cs | 102 ++++++++++++++++++ 1 file changed, 102 insertions(+) create mode 100644 tests/Fallout.Build.Specs/OutputTemplateLevelSpecs.cs diff --git a/tests/Fallout.Build.Specs/OutputTemplateLevelSpecs.cs b/tests/Fallout.Build.Specs/OutputTemplateLevelSpecs.cs new file mode 100644 index 00000000..64d09f05 --- /dev/null +++ b/tests/Fallout.Build.Specs/OutputTemplateLevelSpecs.cs @@ -0,0 +1,102 @@ +using System; +using System.IO; +using System.Linq; +using Fallout.Common.Execution; +using FluentAssertions; +using Serilog.Events; +using Serilog.Formatting.Display; +using Serilog.Parsing; +using Xunit; + +namespace Fallout.Common.Specs; + +/// +/// Covers the level label in the console output templates. See #570. The templates used +/// {Level:u3}, which renders three-letter codes (DBG, INF, WRN) rather +/// than the level names. +/// +public class OutputTemplateLevelSpecs +{ + [Theory] + [InlineData(LogEventLevel.Verbose, "[Verbose]")] + [InlineData(LogEventLevel.Debug, "[Debug]")] + [InlineData(LogEventLevel.Information, "[Information]")] + [InlineData(LogEventLevel.Warning, "[Warning]")] + [InlineData(LogEventLevel.Error, "[Error]")] + [InlineData(LogEventLevel.Fatal, "[Fatal]")] + public void The_standard_template_labels_lines_with_the_level_name(LogEventLevel level, string expected) + { + Render(Logging.StandardOutputTemplate, level).Should().StartWith(expected); + } + + [Theory] + [InlineData(LogEventLevel.Verbose, "[Verbose]")] + [InlineData(LogEventLevel.Debug, "[Debug]")] + [InlineData(LogEventLevel.Information, "[Information]")] + [InlineData(LogEventLevel.Warning, "[Warning]")] + [InlineData(LogEventLevel.Error, "[Error]")] + [InlineData(LogEventLevel.Fatal, "[Fatal]")] + public void The_errors_and_warnings_template_labels_lines_with_the_level_name(LogEventLevel level, string expected) + { + Render(Logging.ErrorsAndWarningsOutputTemplate, level).Should().StartWith(expected); + } + + [Theory] + [InlineData("VRB")] + [InlineData("DBG")] + [InlineData("INF")] + [InlineData("WRN")] + [InlineData("ERR")] + [InlineData("FTL")] + public void No_console_template_renders_a_three_letter_code(string abbreviation) + { + var rendered = Enum.GetValues() + .SelectMany(x => new[] + { + Render(Logging.StandardOutputTemplate, x), + Render(Logging.ErrorsAndWarningsOutputTemplate, x), + Render(Logging.TimestampOutputTemplate, x) + }); + + rendered.Should().NotContain(x => x.Contains($"[{abbreviation}]")); + } + + [Fact] + public void The_timestamped_template_keeps_the_timestamp_ahead_of_the_level() + { + Render(Logging.TimestampOutputTemplate, LogEventLevel.Information) + .Should().MatchRegex(@"^\d{2}:\d{2}:\d{2} \[Information\] "); + } + + [Fact] + public void The_message_still_follows_the_level() + { + Render(Logging.StandardOutputTemplate, LogEventLevel.Warning).Should().Contain("a warning line"); + } + + [Fact] + public void The_errors_and_warnings_template_still_names_the_executing_target() + { + Render(Logging.ErrorsAndWarningsOutputTemplate, LogEventLevel.Error).Should().Contain("Compile:"); + } + + /// Renders one event through an output template, the way the console sink does. + private static string Render(string outputTemplate, LogEventLevel level) + { + var formatter = new MessageTemplateTextFormatter(outputTemplate); + using var writer = new StringWriter(); + formatter.Format(CreateLogEvent(level), writer); + return writer.ToString(); + } + + private static LogEvent CreateLogEvent(LogEventLevel level) => + new( + timestamp: DateTimeOffset.UnixEpoch, + level: level, + exception: null, + messageTemplate: new MessageTemplateParser().Parse("a warning line"), + properties: new[] + { + new LogEventProperty("ExecutingTarget", new ScalarValue("Compile")) + }); +} From b8b9673e0d1d5044bfc544b93047965e99e0ac36 Mon Sep 17 00:00:00 2001 From: Chrison Simtian Date: Mon, 27 Jul 2026 01:43:43 +1200 Subject: [PATCH 2/2] Label console lines with the full level name Replace {Level:u3} with {Level} in the two console output templates, so lines read [Debug] and [Information] instead of [DBG] and [INF]. The codes bought a fixed-width level column. That alignment is not preserved here: laying out a real column belongs to #391, where Spectre owns the frame and can indent instead of truncating the words. Scope is the console templates. The build.log file templates keep their compact single-letter column format. Closes #570 Co-Authored-By: Claude Opus 5 (1M context) --- src/Fallout.Build/Logging.cs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/Fallout.Build/Logging.cs b/src/Fallout.Build/Logging.cs index 5a71665c..7ab11c32 100644 --- a/src/Fallout.Build/Logging.cs +++ b/src/Fallout.Build/Logging.cs @@ -24,8 +24,12 @@ public static class Logging ? AnsiConsoleHostTheme.Default256AnsiColorTheme : SystemConsoleHostTheme.DefaultSystemColorTheme; - internal static string ErrorsAndWarningsOutputTemplate => "[{Level:u3}] {ExecutingTarget}: {Message:l}{NewLine}"; - internal static string StandardOutputTemplate => "[{Level:u3}] {Message:l}{NewLine}{Exception}"; + // Full level names, not the {Level:u3} codes. Abbreviating bought a fixed-width level column, but + // DBG/VRB/FTL are not self-explaining. Aligning the message column is #391's job — Spectre owns + // the frame there and can indent properly instead of truncating the words. The build.log + // templates below deliberately keep their compact single-letter column. + internal static string ErrorsAndWarningsOutputTemplate => "[{Level}] {ExecutingTarget}: {Message:l}{NewLine}"; + internal static string StandardOutputTemplate => "[{Level}] {Message:l}{NewLine}{Exception}"; internal static string TimestampOutputTemplate => $"{{Timestamp:HH:mm:ss}} {StandardOutputTemplate}"; private const int TargetNameLength = 20;