From bf3be9a3cb082b61c5c67f3a98f6dad20a4fcd88 Mon Sep 17 00:00:00 2001 From: "naiyuantian@microsoft.com" Date: Tue, 14 Apr 2026 18:29:23 -0700 Subject: [PATCH 1/5] initial commit --- .../ContinueAsNewTests.cs | 206 ++++++++++++++++++ 1 file changed, 206 insertions(+) create mode 100644 test/InProcessTestHost.Tests/ContinueAsNewTests.cs diff --git a/test/InProcessTestHost.Tests/ContinueAsNewTests.cs b/test/InProcessTestHost.Tests/ContinueAsNewTests.cs new file mode 100644 index 00000000..31692431 --- /dev/null +++ b/test/InProcessTestHost.Tests/ContinueAsNewTests.cs @@ -0,0 +1,206 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Microsoft.DurableTask; +using Microsoft.DurableTask.Client; +using Microsoft.DurableTask.Testing; +using Microsoft.DurableTask.Worker; +using Xunit; +using Xunit.Abstractions; + +namespace InProcessTestHost.Tests; + +/// +/// Tests that orchestrations using ContinueAsNew with activity calls and timers +/// resume correctly after each iteration and eventually complete. +/// +public class ContinueAsNewTests +{ + readonly ITestOutputHelper output; + + public ContinueAsNewTests(ITestOutputHelper output) + { + this.output = output; + } + + /// + /// Registers a polling-style orchestrator and a stateful activity. + /// + /// TestOrchestrator: + /// 1. Calls TestActivity with current state + /// 2. Updates state from activity result + /// 3. If not closed: waits 5s timer, then ContinueAsNew with updated state + /// 4. If closed: orchestration ends + /// + /// TestActivity: + /// - First call: sets status to InProgress + /// - Second call: sets status to Succeeded (triggers orchestration completion) + /// + static void RegisterTestFunctions(DurableTaskRegistry tasks) + { + tasks.AddOrchestratorFunc("TestOrchestrator", async (context, input) => + { + var result = await context.CallActivityAsync("TestActivity", input); + input.Update(result); + + if (!input.Closed) + { + await context.CreateTimer(TimeSpan.FromSeconds(5), CancellationToken.None); + context.ContinueAsNew(input); + return; + } + }); + + tasks.AddActivityFunc("TestActivity", (context, input) => + { + if (input.Status == Status.InProgress) + { + input.Status = Status.Succeeded; + } + else + { + input.Status = Status.InProgress; + } + + return input; + }); + } + + /// + /// Verifies that a single orchestration calling an activity, waiting on a timer, + /// and then using ContinueAsNew completes after 2 iterations without hanging. + /// Covers the basic ContinueAsNew lifecycle: activity call -> state update -> + /// timer -> ContinueAsNew -> activity call -> completion. + /// + [Fact] + public async Task Orchestration_ContinueAsNew_WithActivityAndTimer_CompletesSuccessfully() + { + await using DurableTaskTestHost host = await DurableTaskTestHost.StartAsync(RegisterTestFunctions); + + var input = new AsyncOperation { Status = Status.NotStarted, Closed = false, IterationCount = 0 }; + string instanceId = await host.Client.ScheduleNewOrchestrationInstanceAsync("TestOrchestrator", input); + + using CancellationTokenSource cts = new(TimeSpan.FromSeconds(60)); + OrchestrationMetadata metadata = await host.Client.WaitForInstanceCompletionAsync( + instanceId, getInputsAndOutputs: true, cts.Token); + + Assert.NotNull(metadata); + Assert.Equal(OrchestrationRuntimeStatus.Completed, metadata.RuntimeStatus); + } + + /// + /// Runs 50 ContinueAsNew orchestrations in parallel, repeated across 5 rounds + /// with a fresh host each round. Validates that activity completion messages + /// are correctly delivered under contention when many orchestrations compete + /// for the dispatcher, ready-to-run queue, and instance locks simultaneously. + /// Total: 250 orchestration instances across 5 rounds. + /// + [Fact] + public async Task ConcurrentOrchestrations_ContinueAsNew_AllComplete() + { + const int orchestrationCount = 50; + const int rounds = 5; + + for (int round = 0; round < rounds; round++) + { + this.output.WriteLine($"=== Round {round + 1}/{rounds} ==="); + + await using DurableTaskTestHost host = await DurableTaskTestHost.StartAsync(RegisterTestFunctions); + + string[] instanceIds = new string[orchestrationCount]; + for (int i = 0; i < orchestrationCount; i++) + { + var input = new AsyncOperation { Status = Status.NotStarted, Closed = false, IterationCount = 0 }; + instanceIds[i] = await host.Client.ScheduleNewOrchestrationInstanceAsync("TestOrchestrator", input); + } + + using CancellationTokenSource cts = new(TimeSpan.FromSeconds(60)); + + Task[] waitTasks = instanceIds + .Select(id => host.Client.WaitForInstanceCompletionAsync( + id, getInputsAndOutputs: true, cts.Token)) + .ToArray(); + + OrchestrationMetadata[] results = await Task.WhenAll(waitTasks); + + for (int i = 0; i < orchestrationCount; i++) + { + Assert.NotNull(results[i]); + Assert.Equal(OrchestrationRuntimeStatus.Completed, results[i].RuntimeStatus); + } + + this.output.WriteLine($"Round {round + 1}: all {orchestrationCount} completed"); + } + } + + /// + /// Schedules 100 ContinueAsNew orchestrations on a single host as fast as possible. + /// All 100 share the same dispatcher and ready-to-run queue for their entire lifecycle, + /// maximizing interleaving of activity completion messages and ContinueAsNew + /// re-schedules across instances. + /// + [Fact] + public async Task RapidFire_SequentialScheduling_ContinueAsNew_AllComplete() + { + const int orchestrationCount = 100; + + await using DurableTaskTestHost host = await DurableTaskTestHost.StartAsync(RegisterTestFunctions); + + string[] instanceIds = new string[orchestrationCount]; + for (int i = 0; i < orchestrationCount; i++) + { + var input = new AsyncOperation { Status = Status.NotStarted, Closed = false, IterationCount = 0 }; + instanceIds[i] = await host.Client.ScheduleNewOrchestrationInstanceAsync("TestOrchestrator", input); + } + + this.output.WriteLine($"Scheduled {orchestrationCount} orchestrations on a single host"); + + using CancellationTokenSource cts = new(TimeSpan.FromSeconds(120)); + + Task[] waitTasks = instanceIds + .Select(id => host.Client.WaitForInstanceCompletionAsync( + id, getInputsAndOutputs: true, cts.Token)) + .ToArray(); + + OrchestrationMetadata[] results = await Task.WhenAll(waitTasks); + + for (int i = 0; i < orchestrationCount; i++) + { + Assert.NotNull(results[i]); + Assert.Equal(OrchestrationRuntimeStatus.Completed, results[i].RuntimeStatus); + } + + this.output.WriteLine($"All {orchestrationCount} completed"); + } + + #region Models + + public enum Status + { + NotStarted, + InProgress, + Succeeded, + } + + public class AsyncOperation + { + public Status Status { get; set; } + + public bool Closed { get; set; } + + public int IterationCount { get; set; } + + public void Update(AsyncOperation result) + { + this.Status = result.Status; + this.IterationCount++; + + if (this.Status == Status.Succeeded) + { + this.Closed = true; + } + } + } + + #endregion +} From fe4c7e5f915f69ed62f6240fcc128369a3186546 Mon Sep 17 00:00:00 2001 From: "naiyuantian@microsoft.com" Date: Tue, 14 Apr 2026 19:52:41 -0700 Subject: [PATCH 2/5] update --- .../Sidecar/Grpc/TaskHubGrpcServer.cs | 17 ++++++- .../Sidecar/InMemoryOrchestrationService.cs | 2 +- .../ContinueAsNewTests.cs | 50 +++++++++++++++++++ 3 files changed, 66 insertions(+), 3 deletions(-) diff --git a/src/InProcessTestHost/Sidecar/Grpc/TaskHubGrpcServer.cs b/src/InProcessTestHost/Sidecar/Grpc/TaskHubGrpcServer.cs index 50f1fe15..875b6038 100644 --- a/src/InProcessTestHost/Sidecar/Grpc/TaskHubGrpcServer.cs +++ b/src/InProcessTestHost/Sidecar/Grpc/TaskHubGrpcServer.cs @@ -911,6 +911,19 @@ async Task SendWorkItemToClientAsync(P.WorkItem workItem) { await outputStream.WriteAsync(workItem); } + catch (InvalidOperationException ex) when (ex.Message.Contains("request is complete", StringComparison.OrdinalIgnoreCase)) + { + // The client disconnected or canceled the GetWorkItems stream. + // Reset the connection state so the dispatcher stops trying to write + // to this dead stream and waits for a new client connection instead. + lock (this.isConnectedSignal) + { + this.workerToClientStream = null; + this.isConnectedSignal.Reset(); + } + + throw new OperationCanceledException("Work-item stream closed by client.", ex); + } finally { this.sendWorkItemLock.Release(); @@ -919,7 +932,7 @@ async Task SendWorkItemToClientAsync(P.WorkItem workItem) TaskCompletionSource CreateTaskCompletionSourceForOrchestrator(string instanceId) { - TaskCompletionSource tcs = new(); + TaskCompletionSource tcs = new(TaskCreationOptions.RunContinuationsAsynchronously); this.pendingOrchestratorTasks.TryAdd(instanceId, tcs); return tcs; } @@ -933,7 +946,7 @@ void RemoveOrchestratorTaskCompletionSource(string instanceId) TaskCompletionSource CreateTaskCompletionSourceForActivity(string instanceId, int taskId) { string taskIdKey = GetTaskIdKey(instanceId, taskId); - TaskCompletionSource tcs = new(); + TaskCompletionSource tcs = new(TaskCreationOptions.RunContinuationsAsynchronously); this.pendingActivityTasks.TryAdd(taskIdKey, tcs); return tcs; } diff --git a/src/InProcessTestHost/Sidecar/InMemoryOrchestrationService.cs b/src/InProcessTestHost/Sidecar/InMemoryOrchestrationService.cs index 17d69c5e..81e37311 100644 --- a/src/InProcessTestHost/Sidecar/InMemoryOrchestrationService.cs +++ b/src/InProcessTestHost/Sidecar/InMemoryOrchestrationService.cs @@ -742,7 +742,7 @@ public Task WaitForInstanceAsync(string instanceId, Cancella // First, add the waiter before checking completion to avoid a race condition. // This ensures we don't miss a completion notification that happens between // checking the status and adding the waiter. - var tcs = this.waiters.GetOrAdd(instanceId, _ => new TaskCompletionSource()); + var tcs = this.waiters.GetOrAdd(instanceId, _ => new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously)); // Now check if already completed - if so, complete the waiter immediately if (this.store.TryGetValue(instanceId, out SerializedInstanceState? state)) diff --git a/test/InProcessTestHost.Tests/ContinueAsNewTests.cs b/test/InProcessTestHost.Tests/ContinueAsNewTests.cs index 31692431..307236cc 100644 --- a/test/InProcessTestHost.Tests/ContinueAsNewTests.cs +++ b/test/InProcessTestHost.Tests/ContinueAsNewTests.cs @@ -173,6 +173,56 @@ public async Task RapidFire_SequentialScheduling_ContinueAsNew_AllComplete() this.output.WriteLine($"All {orchestrationCount} completed"); } + /// + /// Schedules 200 ContinueAsNew orchestrations in 10 waves of 20, with 100ms delays + /// between waves. The staggered scheduling creates overlapping lifecycle phases — + /// earlier orchestrations may be mid-ContinueAsNew (waiting on timer or activity) + /// when later waves arrive, producing varied timing patterns that exercise the + /// interplay between lock release, message delivery, and queue re-scheduling. + /// + [Fact] + public async Task Staggered_ContinueAsNew_AllComplete() + { + const int wavesCount = 10; + const int perWave = 20; + + await using DurableTaskTestHost host = await DurableTaskTestHost.StartAsync(RegisterTestFunctions); + + List allInstanceIds = new(); + + for (int wave = 0; wave < wavesCount; wave++) + { + for (int i = 0; i < perWave; i++) + { + var input = new AsyncOperation { Status = Status.NotStarted, Closed = false, IterationCount = 0 }; + string id = await host.Client.ScheduleNewOrchestrationInstanceAsync("TestOrchestrator", input); + allInstanceIds.Add(id); + } + + this.output.WriteLine($"Wave {wave + 1}/{wavesCount}: scheduled {perWave} orchestrations"); + await Task.Delay(100); + } + + this.output.WriteLine($"Total scheduled: {allInstanceIds.Count}"); + + using CancellationTokenSource cts = new(TimeSpan.FromSeconds(180)); + + Task[] waitTasks = allInstanceIds + .Select(id => host.Client.WaitForInstanceCompletionAsync( + id, getInputsAndOutputs: true, cts.Token)) + .ToArray(); + + OrchestrationMetadata[] results = await Task.WhenAll(waitTasks); + + for (int i = 0; i < results.Length; i++) + { + Assert.NotNull(results[i]); + Assert.Equal(OrchestrationRuntimeStatus.Completed, results[i].RuntimeStatus); + } + + this.output.WriteLine($"All {allInstanceIds.Count} completed"); + } + #region Models public enum Status From 016be9d64b75ce335704c254eda23ea71797d416 Mon Sep 17 00:00:00 2001 From: "naiyuantian@microsoft.com" Date: Tue, 14 Apr 2026 21:01:00 -0700 Subject: [PATCH 3/5] update --- .../Sidecar/Grpc/TaskHubGrpcServer.cs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/InProcessTestHost/Sidecar/Grpc/TaskHubGrpcServer.cs b/src/InProcessTestHost/Sidecar/Grpc/TaskHubGrpcServer.cs index 875b6038..65ce691a 100644 --- a/src/InProcessTestHost/Sidecar/Grpc/TaskHubGrpcServer.cs +++ b/src/InProcessTestHost/Sidecar/Grpc/TaskHubGrpcServer.cs @@ -815,7 +815,8 @@ async Task ITaskExecutor.ExecuteOrchestrator( totalBytes += ev.CalculateSize(); } - if (this.supportsHistoryStreaming && totalBytes > (1024)) + const int HistoryStreamingThresholdBytes = 1024 * 1024; // 1 MiB + if (this.supportsHistoryStreaming && totalBytes > HistoryStreamingThresholdBytes) { orkRequest.RequiresHistoryStreaming = true; // Store past events to serve via StreamInstanceHistory @@ -901,8 +902,7 @@ async Task SendWorkItemToClientAsync(P.WorkItem workItem) lock (this.isConnectedSignal) { outputStream = this.workerToClientStream ?? - // CA2201: Use specific exception types - throw new InvalidOperationException("No client is connected. Need to wait until a client connects before executing."); + throw new OperationCanceledException("No client is connected."); } // The gRPC channel can only handle one message at a time, so we need to serialize access to it. @@ -914,15 +914,15 @@ async Task SendWorkItemToClientAsync(P.WorkItem workItem) catch (InvalidOperationException ex) when (ex.Message.Contains("request is complete", StringComparison.OrdinalIgnoreCase)) { // The client disconnected or canceled the GetWorkItems stream. - // Reset the connection state so the dispatcher stops trying to write - // to this dead stream and waits for a new client connection instead. + // Reset the connection state so the dispatcher pauses naturally + // (via the traffic signal) until a new client connects. lock (this.isConnectedSignal) { this.workerToClientStream = null; this.isConnectedSignal.Reset(); } - throw new OperationCanceledException("Work-item stream closed by client.", ex); + this.log.LogWarning(ex, "Work-item stream closed by client. Dispatcher will pause until reconnection."); } finally { From 42c86b4b8d5f75354a775727c99d2a953571fb2d Mon Sep 17 00:00:00 2001 From: "naiyuantian@microsoft.com" Date: Tue, 14 Apr 2026 21:50:18 -0700 Subject: [PATCH 4/5] add test timeout --- .../Sidecar/Grpc/TaskHubGrpcServer.cs | 7 +++- .../ContinueAsNewTests.cs | 40 +++++++++---------- 2 files changed, 25 insertions(+), 22 deletions(-) diff --git a/src/InProcessTestHost/Sidecar/Grpc/TaskHubGrpcServer.cs b/src/InProcessTestHost/Sidecar/Grpc/TaskHubGrpcServer.cs index 65ce691a..28214435 100644 --- a/src/InProcessTestHost/Sidecar/Grpc/TaskHubGrpcServer.cs +++ b/src/InProcessTestHost/Sidecar/Grpc/TaskHubGrpcServer.cs @@ -902,7 +902,7 @@ async Task SendWorkItemToClientAsync(P.WorkItem workItem) lock (this.isConnectedSignal) { outputStream = this.workerToClientStream ?? - throw new OperationCanceledException("No client is connected."); + throw new RpcException(new Status(StatusCode.Unavailable, "No client is connected.")); } // The gRPC channel can only handle one message at a time, so we need to serialize access to it. @@ -922,7 +922,10 @@ async Task SendWorkItemToClientAsync(P.WorkItem workItem) this.isConnectedSignal.Reset(); } - this.log.LogWarning(ex, "Work-item stream closed by client. Dispatcher will pause until reconnection."); + // Must throw so callers (ExecuteOrchestrator/ExecuteActivity) can clean up + // their pending TCS. The dispatcher catches this, abandons the work item, + // and releases it back to the queue for retry. + throw new OperationCanceledException("Work-item stream closed by client.", ex); } finally { diff --git a/test/InProcessTestHost.Tests/ContinueAsNewTests.cs b/test/InProcessTestHost.Tests/ContinueAsNewTests.cs index 307236cc..c5805643 100644 --- a/test/InProcessTestHost.Tests/ContinueAsNewTests.cs +++ b/test/InProcessTestHost.Tests/ContinueAsNewTests.cs @@ -29,7 +29,7 @@ public ContinueAsNewTests(ITestOutputHelper output) /// TestOrchestrator: /// 1. Calls TestActivity with current state /// 2. Updates state from activity result - /// 3. If not closed: waits 5s timer, then ContinueAsNew with updated state + /// 3. If not closed: waits 1s timer, then ContinueAsNew with updated state /// 4. If closed: orchestration ends /// /// TestActivity: @@ -45,7 +45,7 @@ static void RegisterTestFunctions(DurableTaskRegistry tasks) if (!input.Closed) { - await context.CreateTimer(TimeSpan.FromSeconds(5), CancellationToken.None); + await context.CreateTimer(TimeSpan.FromSeconds(1), CancellationToken.None); context.ContinueAsNew(input); return; } @@ -72,7 +72,7 @@ static void RegisterTestFunctions(DurableTaskRegistry tasks) /// Covers the basic ContinueAsNew lifecycle: activity call -> state update -> /// timer -> ContinueAsNew -> activity call -> completion. /// - [Fact] + [Fact(Timeout = 30_000)] public async Task Orchestration_ContinueAsNew_WithActivityAndTimer_CompletesSuccessfully() { await using DurableTaskTestHost host = await DurableTaskTestHost.StartAsync(RegisterTestFunctions); @@ -80,7 +80,7 @@ public async Task Orchestration_ContinueAsNew_WithActivityAndTimer_CompletesSucc var input = new AsyncOperation { Status = Status.NotStarted, Closed = false, IterationCount = 0 }; string instanceId = await host.Client.ScheduleNewOrchestrationInstanceAsync("TestOrchestrator", input); - using CancellationTokenSource cts = new(TimeSpan.FromSeconds(60)); + using CancellationTokenSource cts = new(TimeSpan.FromSeconds(20)); OrchestrationMetadata metadata = await host.Client.WaitForInstanceCompletionAsync( instanceId, getInputsAndOutputs: true, cts.Token); @@ -89,17 +89,17 @@ public async Task Orchestration_ContinueAsNew_WithActivityAndTimer_CompletesSucc } /// - /// Runs 50 ContinueAsNew orchestrations in parallel, repeated across 5 rounds + /// Runs 10 ContinueAsNew orchestrations in parallel, repeated across 3 rounds /// with a fresh host each round. Validates that activity completion messages /// are correctly delivered under contention when many orchestrations compete /// for the dispatcher, ready-to-run queue, and instance locks simultaneously. - /// Total: 250 orchestration instances across 5 rounds. + /// Total: 30 orchestration instances across 3 rounds. /// - [Fact] + [Fact(Timeout = 60_000)] public async Task ConcurrentOrchestrations_ContinueAsNew_AllComplete() { - const int orchestrationCount = 50; - const int rounds = 5; + const int orchestrationCount = 10; + const int rounds = 3; for (int round = 0; round < rounds; round++) { @@ -114,7 +114,7 @@ public async Task ConcurrentOrchestrations_ContinueAsNew_AllComplete() instanceIds[i] = await host.Client.ScheduleNewOrchestrationInstanceAsync("TestOrchestrator", input); } - using CancellationTokenSource cts = new(TimeSpan.FromSeconds(60)); + using CancellationTokenSource cts = new(TimeSpan.FromSeconds(30)); Task[] waitTasks = instanceIds .Select(id => host.Client.WaitForInstanceCompletionAsync( @@ -134,15 +134,15 @@ public async Task ConcurrentOrchestrations_ContinueAsNew_AllComplete() } /// - /// Schedules 100 ContinueAsNew orchestrations on a single host as fast as possible. - /// All 100 share the same dispatcher and ready-to-run queue for their entire lifecycle, + /// Schedules 20 ContinueAsNew orchestrations on a single host as fast as possible. + /// All 20 share the same dispatcher and ready-to-run queue for their entire lifecycle, /// maximizing interleaving of activity completion messages and ContinueAsNew /// re-schedules across instances. /// - [Fact] + [Fact(Timeout = 60_000)] public async Task RapidFire_SequentialScheduling_ContinueAsNew_AllComplete() { - const int orchestrationCount = 100; + const int orchestrationCount = 20; await using DurableTaskTestHost host = await DurableTaskTestHost.StartAsync(RegisterTestFunctions); @@ -155,7 +155,7 @@ public async Task RapidFire_SequentialScheduling_ContinueAsNew_AllComplete() this.output.WriteLine($"Scheduled {orchestrationCount} orchestrations on a single host"); - using CancellationTokenSource cts = new(TimeSpan.FromSeconds(120)); + using CancellationTokenSource cts = new(TimeSpan.FromSeconds(30)); Task[] waitTasks = instanceIds .Select(id => host.Client.WaitForInstanceCompletionAsync( @@ -174,17 +174,17 @@ public async Task RapidFire_SequentialScheduling_ContinueAsNew_AllComplete() } /// - /// Schedules 200 ContinueAsNew orchestrations in 10 waves of 20, with 100ms delays + /// Schedules 30 ContinueAsNew orchestrations in 3 waves of 10, with 100ms delays /// between waves. The staggered scheduling creates overlapping lifecycle phases — /// earlier orchestrations may be mid-ContinueAsNew (waiting on timer or activity) /// when later waves arrive, producing varied timing patterns that exercise the /// interplay between lock release, message delivery, and queue re-scheduling. /// - [Fact] + [Fact(Timeout = 60_000)] public async Task Staggered_ContinueAsNew_AllComplete() { - const int wavesCount = 10; - const int perWave = 20; + const int wavesCount = 3; + const int perWave = 10; await using DurableTaskTestHost host = await DurableTaskTestHost.StartAsync(RegisterTestFunctions); @@ -205,7 +205,7 @@ public async Task Staggered_ContinueAsNew_AllComplete() this.output.WriteLine($"Total scheduled: {allInstanceIds.Count}"); - using CancellationTokenSource cts = new(TimeSpan.FromSeconds(180)); + using CancellationTokenSource cts = new(TimeSpan.FromSeconds(30)); Task[] waitTasks = allInstanceIds .Select(id => host.Client.WaitForInstanceCompletionAsync( From 449bd2493dff8b880cd6baa562b6ac646c0acfb3 Mon Sep 17 00:00:00 2001 From: Varshi Bachu Date: Wed, 15 Apr 2026 13:18:31 -0700 Subject: [PATCH 5/5] update version --- src/InProcessTestHost/InProcessTestHost.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/InProcessTestHost/InProcessTestHost.csproj b/src/InProcessTestHost/InProcessTestHost.csproj index 9cdc750c..5114e3df 100644 --- a/src/InProcessTestHost/InProcessTestHost.csproj +++ b/src/InProcessTestHost/InProcessTestHost.csproj @@ -5,7 +5,7 @@ Microsoft.DurableTask.Testing Microsoft.DurableTask.InProcessTestHost Microsoft.DurableTask.InProcessTestHost - 0.2.1-preview.1 + 0.2.2-preview.1 $(NoWarn);CA1848