Files
vfxreview/RenderWorker/VFXReviewWorker.Tests/BlenderProgressParserTests.cs
twotalesanimation ed8a225106
Deploy / deploy (push) Successful in 6m5s
fix(blender): read carriage-return progress, and let any output prove liveness
Blender renders failed with "produced no output for 600s" on every
attempt while rendering perfectly well.

Two causes, both mine. BeginOutputReadLine raises an event per newline,
and Blender writes its render status with a carriage return so it
overwrites one line in place — a frame taking longer than the stall
timeout emits no newline for its whole duration. stdout is now read as a
character stream with CR and LF both ending a line.

The watchdog was also stricter than its own message: it only accepted a
recognised Fra:/Saved: line as a sign of life, so any output we were not
parsing counted as silence. Any output now proves Blender is alive, which
is what "produced no output" is asking; finished frames still drive
progress, since a single frame emits hundreds of status updates.

Also passes -noaudio: a render node has no sound device, and initialising
audio under a session-less service account is a known way to hang.

Covered by tests over a realistic stdout chunk where progress is
CR-separated and only the saved lines end in LF — newline-only splitting
yields one unusable blob and no progress at all.

Worker 1.5.1.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-15 20:44:49 +02:00

93 lines
3.4 KiB
C#

using System.Text;
using VFXReviewWorker;
using Xunit;
namespace VFXReviewWorker.Tests;
/// <summary>
/// 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.
/// </summary>
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));
}
/// <summary>
/// The regression itself: a realistic stdout chunk where progress is
/// separated by CR and only the saved lines end in LF.
/// </summary>
[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]);
}
/// <summary>Mirrors BlenderRunner.PumpAsync: CR and LF both end a line.</summary>
private static List<string> SplitLikeThePump(string text)
{
var lines = new List<string>();
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;
}
}