Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -217,26 +217,32 @@ private async Task<IHost> BuildServerTestHostAsync(BuildContext context, NamedPi
(TestFrameworkManager)TestFramework!,
(TestHostManager)TestHost);

// Register ServerTelemetry in the service provider so that all telemetry events in server mode
// are forwarded to the client via JSON-RPC. This replaces the NopTelemetryService registered
// earlier in SetupCommonServicesAsync. This is always registered regardless of whether platform
// telemetry is enabled or disabled, because server-mode telemetry is a protocol concern (JSON-RPC
// forwarding to the client), not a data-collection concern.
context.ServiceProvider.ReplaceService<ITelemetryCollector>(new ServerTelemetry(serverTestHost));

#pragma warning disable CA1416 // Preserve existing browser behavior while splitting the method.
IHost actualTestHost = testControllerConnection is not null
? new TestHostControlledHost(testControllerConnection, serverTestHost, context.TestApplicationCancellationTokenSource.CancellationToken)
: serverTestHost;
#pragma warning restore CA1416 // Preserve existing browser behavior while splitting the method.

// The TestHostBuilt telemetry event must be sent through the collector previously registered
// by TelemetryManager in SetupCommonServicesAsync (a real collector when telemetry is opted in,
// or NopTelemetryService when opted out) because the ServerTestHost JSON-RPC message handler is
// not yet initialized at this point (it is created later inside ServerTestHost.InternalRunAsync).
// Sending it through ServerTelemetry here would throw InvalidOperationException via
// ServerTestHost.AssertInitialized.
await LogTestHostCreatedAsync(
context.ServiceProvider,
TelemetryProperties.ApplicationMode.Server,
context.BuilderMetrics,
context.SystemClock.UtcNow,
context.TestApplicationCancellationTokenSource.CancellationToken).ConfigureAwait(false);

// Register ServerTelemetry in the service provider so that all telemetry events raised at runtime
// in server mode are forwarded to the client via JSON-RPC. This replaces the collector registered
// earlier by TelemetryManager in SetupCommonServicesAsync. This is always registered regardless of
// whether platform telemetry is enabled or disabled, because server-mode telemetry is a protocol
// concern (JSON-RPC forwarding to the client), not a data-collection concern.
context.ServiceProvider.ReplaceService<ITelemetryCollector>(new ServerTelemetry(serverTestHost));

CompleteBuilderActivity(context.BuilderActivity, testControllerConnection is not null ? nameof(TestHostControlledHost) : nameof(ServerTestHost));
return actualTestHost;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
<ItemGroup>
<!-- Embedded helpers from Microsoft.Testing.Platform -->
<Compile Include="$(RepoRoot)src\Platform\Microsoft.Testing.Platform\Helpers\ApplicationStateGuard.cs" Link="Helpers\ApplicationStateGuard.cs" />
<Compile Include="$(RepoRoot)src\Platform\Microsoft.Testing.Platform\Helpers\EnvironmentVariableConstants.cs" Link="Helpers\EnvironmentVariableConstants.cs" />
<Compile Include="$(RepoRoot)src\Platform\Microsoft.Testing.Platform\Helpers\ExitCodes.cs" Link="Helpers\ExitCodes.cs" />
<Compile Include="$(RepoRoot)src\Platform\Microsoft.Testing.Platform\Resources\PlatformResources.cs" Link="Resources\PlatformResources.cs" />
</ItemGroup>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

using Microsoft.Testing.Platform.ServerMode.IntegrationTests.Messages.V100;
Expand All @@ -10,6 +10,35 @@ namespace Microsoft.Testing.Platform.Acceptance.IntegrationTests;
[TestClass]
public sealed partial class ServerLoggingTests : ServerModeTestsBase<ServerLoggingTests.TestAssetFixture>
{
[TestMethod]
public async Task RunningInServerJsonRpcModeWithTelemetryEnabled_DoesNotCrashDuringBuildAsync()
{
// Regression test: PR #8211 registered ServerTelemetry in the service provider BEFORE
// calling LogTestHostCreatedAsync. Because ServerTestHost's JSON-RPC message handler is
// only initialized inside InternalRunAsync, the build-time TestHostBuilt telemetry event
// attempted to flow through the not-yet-initialized server, causing
// ServerTestHost.AssertInitialized to throw InvalidOperationException during
// TestApplicationBuilder.BuildAsync().
//
// This test forces telemetry to be opted in (overriding any DOTNET_CLI_TELEMETRY_OPTOUT
// inherited from Arcade in CI) so the LogTestHostCreatedAsync path is actually
// executed and would crash without the fix.
string tfm = TargetFrameworks.NetCurrent;
var testHost = TestInfrastructure.TestHost.LocateFrom(AssetFixture.TargetAssetPath, "ServerLoggingTests", tfm);
Dictionary<string, string?> telemetryEnabledEnv = new()
{
[EnvironmentVariableConstants.DOTNET_CLI_TELEMETRY_OPTOUT] = "0",
[EnvironmentVariableConstants.TESTINGPLATFORM_TELEMETRY_OPTOUT] = "0",
};
Comment thread
Evangelink marked this conversation as resolved.
using TestingPlatformClient jsonClient = await StartAsServerAndConnectToTheClientAsync(testHost, telemetryEnabledEnv);

// If the host crashed during BuildAsync, Initialize would never get a response and time out.
InitializeResponse initializeResponseArgs = await jsonClient.Initialize();
Assert.IsNotNull(initializeResponseArgs);

await jsonClient.Exit();
}

[TestMethod]
public async Task RunningInServerJsonRpcModeShouldHaveOutputDeviceLogsPushedToTestExplorer()
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
using System.Net.Sockets;

using Microsoft.Testing.Platform.Acceptance.IntegrationTests;
using Microsoft.Testing.Platform.Helpers;
using Microsoft.Testing.Platform.ServerMode.IntegrationTests.Messages.V100;

namespace MSTest.Acceptance.IntegrationTests.Messages.V100;
Expand All @@ -21,7 +22,10 @@ namespace MSTest.Acceptance.IntegrationTests.Messages.V100;
{ "DOTNET_MULTILEVEL_LOOKUP", "0" },
};

protected async Task<TestingPlatformClient> StartAsServerAndConnectToTheClientAsync(TestHost testHost)
protected Task<TestingPlatformClient> StartAsServerAndConnectToTheClientAsync(TestHost testHost)
=> StartAsServerAndConnectToTheClientAsync(testHost, additionalEnvironmentVariables: null);

protected async Task<TestingPlatformClient> StartAsServerAndConnectToTheClientAsync(TestHost testHost, IReadOnlyDictionary<string, string?>? additionalEnvironmentVariables)
{
var environmentVariables = new Dictionary<string, string?>(DefaultEnvironmentVariables);
foreach (DictionaryEntry entry in Environment.GetEnvironmentVariables())
Expand All @@ -37,7 +41,16 @@ protected async Task<TestingPlatformClient> StartAsServerAndConnectToTheClientAs
}

// We expect to not fail for unhandled exception in server mode for IDE needs.
environmentVariables.Add("TESTINGPLATFORM_EXIT_PROCESS_ON_UNHANDLED_EXCEPTION", "0");
// Use indexer assignment so that an inherited value (if any) is safely overridden.
environmentVariables[EnvironmentVariableConstants.TESTINGPLATFORM_EXIT_PROCESS_ON_UNHANDLED_EXCEPTION] = "0";

if (additionalEnvironmentVariables is not null)
{
foreach (KeyValuePair<string, string?> kvp in additionalEnvironmentVariables)
{
environmentVariables[kvp.Key] = kvp.Value;
}
}

// To attach to the server on startup
// environmentVariables.Add(EnvironmentVariableConstants.TESTINGPLATFORM_LAUNCH_ATTACH_DEBUGGER, "1");
Expand Down