using System.Threading.Tasks;
using ZB.MOM.WW.ScadaBridge.CLI.Commands;
namespace ZB.MOM.WW.ScadaBridge.CLI.Tests;
///
/// Regression tests for the testable pieces of DebugCommands.StreamDebugAsync:
/// Ctrl+C during connect being misreported as a connection failure, and
/// non-deterministic exit codes after stream termination.
///
public class DebugStreamTests
{
// --- connect-failure classification --------------------------------------
/// Verifies that an raised after cancellation was requested is treated as a graceful cancellation.
[Fact]
public void ClassifyConnectFailure_OperationCanceled_IsTreatedAsCancellation()
{
// Ctrl+C while StartAsync is still establishing the connection throws
// OperationCanceledException — this is a graceful cancellation, not a failure.
var result = DebugStreamHelpers.ClassifyConnectFailure(
new OperationCanceledException(), cancellationRequested: true);
Assert.True(result.IsCancellation);
Assert.Equal(0, result.ExitCode);
}
/// Verifies that a raised after cancellation was requested is treated as a cancellation.
[Fact]
public void ClassifyConnectFailure_TaskCanceled_WhenCancelRequested_IsCancellation()
{
var result = DebugStreamHelpers.ClassifyConnectFailure(
new TaskCanceledException(), cancellationRequested: true);
Assert.True(result.IsCancellation);
Assert.Equal(0, result.ExitCode);
}
/// Verifies that a genuine connection exception is classified as a connection failure with exit code 1.
[Fact]
public void ClassifyConnectFailure_RealException_IsConnectionFailure()
{
var result = DebugStreamHelpers.ClassifyConnectFailure(
new HttpRequestException("connection refused"), cancellationRequested: false);
Assert.False(result.IsCancellation);
Assert.Equal(1, result.ExitCode);
}
/// Verifies that a cancellation exception without a user cancel request is classified as a connection failure.
[Fact]
public void ClassifyConnectFailure_CanceledExceptionButNoCancelRequested_IsConnectionFailure()
{
// A cancellation that did not originate from the user (e.g. a server-side abort)
// is still a real connection failure.
var result = DebugStreamHelpers.ClassifyConnectFailure(
new OperationCanceledException(), cancellationRequested: false);
Assert.False(result.IsCancellation);
Assert.Equal(1, result.ExitCode);
}
// --- deterministic exit-code resolution ----------------------------------
/// Verifies that a result already set by stream termination is preferred when resolving the exit code.
/// A task that represents the asynchronous operation.
[Fact]
public async Task ResolveStreamExitCodeAsync_TerminationResultSet_PrefersThatResult()
{
// OnStreamTerminated set exitTcs to 1 — that must win even on the Ctrl+C path.
var tcs = new TaskCompletionSource();
tcs.SetResult(1);
var code = await DebugStreamHelpers.ResolveStreamExitCodeAsync(tcs.Task);
Assert.Equal(1, code);
}
/// Verifies that the exit code resolves to zero when no termination result was ever set.
/// A task that represents the asynchronous operation.
[Fact]
public async Task ResolveStreamExitCodeAsync_NoResult_ReturnsZero()
{
// Pure Ctrl+C: exitTcs never completed — graceful shutdown, exit 0.
var tcs = new TaskCompletionSource();
var code = await DebugStreamHelpers.ResolveStreamExitCodeAsync(tcs.Task);
Assert.Equal(0, code);
}
/// Verifies that a result arriving during the grace period is observed when resolving the exit code.
/// A task that represents the asynchronous operation.
[Fact]
public async Task ResolveStreamExitCodeAsync_ResultArrivesDuringGrace_IsObserved()
{
// A stream termination racing with Ctrl+C: the result lands shortly after the
// wait was cancelled. The grace period must let it be observed deterministically.
var tcs = new TaskCompletionSource();
_ = Task.Run(async () =>
{
await Task.Delay(20);
tcs.TrySetResult(1);
});
var code = await DebugStreamHelpers.ResolveStreamExitCodeAsync(tcs.Task);
Assert.Equal(1, code);
}
}