using System.Text; using VFXReviewWorker; using Xunit; namespace VFXReviewWorker.Tests; /// /// Blender writes its render status with a carriage return so it overwrites one /// line in the terminal, and only emits a newline when a frame is saved. A /// reader that splits on newlines alone therefore sees nothing at all while a /// long frame renders — which had the stall watchdog killing healthy jobs. /// public class BlenderProgressParserTests { [Theory] [InlineData("Fra:1042 Mem:512.00M (Peak 640.00M) | Time:00:12.34 | Path Tracing Sample 12/128", 1042)] [InlineData("Fra:1 Mem:64.00M | Syncing Cube", 1)] [InlineData("Saved: 'X:/renders/frame_1042.exr'", null)] [InlineData("Blender quit", null)] public void ParsesTheFrameBeingRendered(string line, int? expected) { Assert.Equal(expected, BlenderProgressParser.ParseFrame(line)); } [Theory] [InlineData("Saved: 'X:/renders/frame_1042.exr'", "X:/renders/frame_1042.exr")] [InlineData(" Saved: 'nope'", null)] // leading space: not the real form [InlineData("Fra:1042 Mem:512.00M", null)] public void ParsesSavedFrames(string line, string? expected) { Assert.Equal(expected, BlenderProgressParser.ParseSaved(line)); } [Fact] public void TreatsAMissingBlendAsNonRetryable() { const string line = "Error: Cannot read file 'X:/gone.blend': No such file or directory"; Assert.True(BlenderProgressParser.IsErrorLine(line)); Assert.True(BlenderProgressParser.IsNonRetryableError(line)); } [Fact] public void TreatsAnUnknownFailureAsRetryable() { const string line = "Error: CUDA device lost, out of memory"; Assert.True(BlenderProgressParser.IsErrorLine(line)); Assert.False(BlenderProgressParser.IsNonRetryableError(line)); } /// /// The regression itself: a realistic stdout chunk where progress is /// separated by CR and only the saved lines end in LF. /// [Fact] public void CarriageReturnProgressIsSeenWithoutWaitingForANewline() { var stdout = "Fra:1001 Mem:64.00M | Sample 1/128\r" + "Fra:1001 Mem:120.00M | Sample 64/128\r" + "Fra:1001 Mem:128.00M | Sample 128/128\r" + "\nSaved: 'X:/renders/frame_1001.exr'\n" + "Fra:1002 Mem:128.00M | Sample 1/128\r"; var lines = SplitLikeThePump(stdout); var frames = lines.Select(BlenderProgressParser.ParseFrame).Where(f => f is not null).ToList(); var saved = lines.Select(BlenderProgressParser.ParseSaved).Where(s => s is not null).ToList(); // Four status updates and one saved frame; splitting on "\n" alone // would have yielded a single unusable blob and no progress at all. Assert.Equal(4, frames.Count); Assert.Single(saved); Assert.Equal(1002, frames[^1]); } /// Mirrors BlenderRunner.PumpAsync: CR and LF both end a line. private static List SplitLikeThePump(string text) { var lines = new List(); var current = new StringBuilder(); foreach (var c in text) { if (c == '\r' || c == '\n') { if (current.Length > 0) { lines.Add(current.ToString()); current.Clear(); } } else current.Append(c); } if (current.Length > 0) lines.Add(current.ToString()); return lines; } }