7b0b9c7365
Solution + 23 src projects + 26 test projects renamed; folders, csproj, namespaces, and ScadaLinkDbContext/ScadaBridgeDbContext class updated. ActorSystem "scadalink" → "scadabridge", Akka seed-node URLs migrated. SQL roles/logins, LDAP domains, CLI command name, and CLI config dir (~/.scadalink → ~/.scadabridge) also renamed. Build green; 5 Host.Tests fail awaiting SQL login rename in next commit. Pre-existing StaleTagMonitor timing flakes unchanged. Rename script committed at tools/rename-to-scadabridge.sh.
87 lines
3.4 KiB
C#
87 lines
3.4 KiB
C#
using ZB.MOM.WW.ScadaBridge.CentralUI.ScriptAnalysis;
|
|
|
|
namespace ZB.MOM.WW.ScadaBridge.CentralUI.Tests.ScriptAnalysis;
|
|
|
|
/// <summary>
|
|
/// Regression tests for the <c>SandboxConsoleCapture</c> writer that the Test Run
|
|
/// sandbox installs on <c>Console.Out</c>/<c>Console.Error</c>. CentralUI-030
|
|
/// surfaced an intra-script concurrency hazard: a sandboxed script can fan out
|
|
/// work with <c>Task.WhenAll</c> / <c>Task.Run</c> and every child task inherits
|
|
/// the capture <c>StringWriter</c> via <c>AsyncLocal</c>; <c>StringWriter</c> is
|
|
/// not thread-safe, so concurrent writes could corrupt the buffer. These tests
|
|
/// drive the writer the same way Roslyn-hosted user code does.
|
|
/// </summary>
|
|
public class SandboxConsoleCaptureTests
|
|
{
|
|
/// <summary>
|
|
/// CentralUI-030: a capture scope shared across <c>Task.WhenAll</c> child
|
|
/// tasks must serialise writes so the resulting transcript contains exactly
|
|
/// the expected number of lines without character-level interleaving.
|
|
/// </summary>
|
|
[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);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Sanity check: the most basic capture happy-path still works after the
|
|
/// CentralUI-030 lock was introduced.
|
|
/// </summary>
|
|
[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());
|
|
}
|
|
}
|