using ZB.MOM.WW.ScadaBridge.CentralUI.ScriptAnalysis;
namespace ZB.MOM.WW.ScadaBridge.CentralUI.Tests.ScriptAnalysis;
///
/// Regression tests for the SandboxConsoleCapture writer that the Test Run
/// sandbox installs on Console.Out/Console.Error. CentralUI-030
/// surfaced an intra-script concurrency hazard: a sandboxed script can fan out
/// work with Task.WhenAll / Task.Run and every child task inherits
/// the capture StringWriter via AsyncLocal; StringWriter is
/// not thread-safe, so concurrent writes could corrupt the buffer. These tests
/// drive the writer the same way Roslyn-hosted user code does.
///
public class SandboxConsoleCaptureTests
{
///
/// CentralUI-030: a capture scope shared across Task.WhenAll child
/// tasks must serialise writes so the resulting transcript contains exactly
/// the expected number of lines without character-level interleaving.
///
[Fact]
public async Task BeginCapture_ConcurrentWritesFromTasks_DoNotCorruptBuffer()
{
// The static install routes Console.Out through the singleton sandbox
// capture writer for the test process — this is idempotent and matches
// the way ScriptAnalysisService bootstraps the sandbox in production.
var (capture, _) = SandboxConsoleCapture.Install();
var buffer = new StringWriter();
const int taskCount = 32;
const int linesPerTask = 50;
const int expectedLines = taskCount * linesPerTask;
using (capture.BeginCapture(buffer))
{
// AsyncLocal flows the capture scope into each Task.Run, mirroring
// a sandboxed script doing `await Task.WhenAll(...)` over Tasks
// that each `Console.WriteLine`.
var tasks = Enumerable.Range(0, taskCount).Select(i => Task.Run(() =>
{
for (var j = 0; j < linesPerTask; j++)
{
Console.WriteLine($"task-{i}-line-{j}");
}
}));
await Task.WhenAll(tasks);
}
var captured = buffer.ToString();
// Without the lock, concurrent StringWriter.WriteLine can drop or
// interleave characters and produce malformed lines / a wrong count.
// We assert the exact line count and that every emitted token is
// present on a line of its own — both fail under the unprotected
// implementation.
var lines = captured.Split(Environment.NewLine, StringSplitOptions.RemoveEmptyEntries);
Assert.Equal(expectedLines, lines.Length);
for (var i = 0; i < taskCount; i++)
{
for (var j = 0; j < linesPerTask; j++)
{
Assert.Contains($"task-{i}-line-{j}", lines);
}
}
}
///
/// Sanity check: the most basic capture happy-path still works after the
/// CentralUI-030 lock was introduced.
///
[Fact]
public void BeginCapture_SingleThreadedWrites_AreCaptured()
{
var (capture, _) = SandboxConsoleCapture.Install();
var buffer = new StringWriter();
using (capture.BeginCapture(buffer))
{
Console.WriteLine("hello");
Console.Write("world");
}
Assert.Contains("hello", buffer.ToString());
Assert.Contains("world", buffer.ToString());
}
}