Sitelet https://github.com/microsoft/testfx/commit/79f26ff23fac8769c84783fe2fa0cfc20134bcf8
Skip to content

Commit 79f26ff

Browse files
nohwnd-botEvangelinkCopilot
authored
Fix forward-compat crash loading old 2.x extensions on .NET Framework (#9710) by @Evangelink in #9739 (backport to rel/4.3) (#9740)
Co-authored-by: Amaury Levé <amauryleve@microsoft.com> Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
1 parent 9b1c86d commit 79f26ff

2 files changed

Lines changed: 218 additions & 0 deletions

File tree

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
// <auto-generated />
2+
3+
#if !NETCOREAPP
4+
5+
#pragma warning disable
6+
7+
using System;
8+
using System.Collections.Generic;
9+
using System.IO;
10+
using System.Text;
11+
using System.Threading;
12+
using System.Threading.Tasks;
13+
using System.Xml.Linq;
14+
15+
namespace Polyfills;
16+
17+
// IMPORTANT (see https://github.com/microsoft/testfx/issues/9710):
18+
// These Polyfill helpers are intentionally kept even though newer Microsoft.Testing.Platform code no
19+
// longer calls them. Older (2.x) extension assemblies - e.g. Microsoft.Testing.Extensions.Telemetry,
20+
// TrxReport, TrxReport.Abstractions and VSTestBridge - were compiled against these members and access
21+
// them from the platform assembly via [InternalsVisibleTo] (they did not embed their own copy). Their
22+
// baked-in member references are resolved by the runtime against THIS assembly, so removing any of the
23+
// members below would reintroduce a MissingMethodException when a newer platform loads an older
24+
// extension's netstandard2.0 / .NET Framework asset (the .NET assets bind these to the BCL and are not
25+
// affected). Do NOT delete these as "unused polyfills".
26+
internal static partial class Polyfill
27+
{
28+
public static void Deconstruct<TKey, TValue>(this KeyValuePair<TKey, TValue> pair, out TKey key, out TValue value)
29+
{
30+
key = pair.Key;
31+
value = pair.Value;
32+
}
33+
34+
public static bool Contains(this string target, char value)
35+
=> target.IndexOf(value) != -1;
36+
37+
public static bool Contains(this string target, string value, StringComparison comparisonType)
38+
=> target.IndexOf(value, comparisonType) != -1;
39+
40+
public static StringBuilder AppendJoin(this StringBuilder target, string separator, params string[] values)
41+
=> target.Append(string.Join(separator, values));
42+
43+
public static StringBuilder AppendJoin<T>(this StringBuilder target, string separator, IEnumerable<T> values)
44+
=> target.Append(string.Join(separator, values));
45+
46+
public static Task CopyToAsync(this Stream source, Stream destination, CancellationToken cancellationToken)
47+
=> source.CopyToAsync(destination, 81920, cancellationToken);
48+
49+
public static Task SaveAsync(this XDocument target, Stream stream, SaveOptions options, CancellationToken cancellationToken)
50+
{
51+
cancellationToken.ThrowIfCancellationRequested();
52+
target.Save(stream, options);
53+
return Task.CompletedTask;
54+
}
55+
}
56+
#endif
Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
3+
4+
namespace Microsoft.Testing.Platform.Acceptance.IntegrationTests;
5+
6+
/// <summary>
7+
/// Regression coverage for <see href="https://github.com/microsoft/testfx/issues/9710"/>.
8+
///
9+
/// <para>
10+
/// A newer <c>Microsoft.Testing.Platform</c> combined with the OLDEST supported 2.x
11+
/// <c>Microsoft.Testing.Extensions.Telemetry</c> must still load and run. The old telemetry
12+
/// extension's <c>netstandard2.0</c> asset was compiled against the platform's internal
13+
/// <c>Polyfills.Polyfill</c> helper (shared via <c>InternalsVisibleTo</c> before polyfills became
14+
/// per-assembly embedded types), so its IL carries a reference to
15+
/// <c>[Microsoft.Testing.Platform]Polyfills.Polyfill.Deconstruct(KeyValuePair&lt;,&gt;, out, out)</c>.
16+
/// When that member is missing from the newer platform, <c>AppInsightsProvider.IngestLoopAsync</c>
17+
/// throws <c>MissingMethodException</c> on its background thread and crashes the process.
18+
/// </para>
19+
///
20+
/// <para>
21+
/// Two conditions are required to surface the crash, and both were missing from
22+
/// <see cref="ForwardCompatibilityTests"/>:
23+
/// <list type="number">
24+
/// <item>The <b>.NET Framework</b> host (<c>net462</c>) must be used so NuGet resolves the telemetry
25+
/// extension's <c>netstandard2.0</c> asset (the .NET assets bind <c>Deconstruct</c> to the BCL and are
26+
/// not affected). The original report crashed on <c>net472</c> while <c>net8.0</c> passed.</item>
27+
/// <item>Telemetry must be <b>enabled</b> so <c>TelemetryManager</c> actually constructs
28+
/// <c>AppInsightsProvider</c> and runs its ingest loop. When opted out (the acceptance default),
29+
/// a <c>NopTelemetryService</c> is used and the crashing method never runs.</item>
30+
/// </list>
31+
/// </para>
32+
/// </summary>
33+
[TestClass]
34+
public class TelemetryForwardCompatibilityTests : AcceptanceTestBase<TelemetryForwardCompatibilityTests.TestAssetFixture>
35+
{
36+
private const string AssetName = "TelemetryForwardCompatibilityTest";
37+
38+
// The crash is specific to the netstandard2.0 telemetry asset that ships with old 2.x extensions,
39+
// which is only resolved on the .NET Framework host. That host only exists on Windows.
40+
[TestMethod]
41+
[OSCondition(OperatingSystems.Windows)]
42+
public async Task NewerPlatform_WithOldestSupportedTelemetryExtension_TelemetryEnabled_OnNetFramework_ShouldNotCrash()
43+
{
44+
var testHost = TestInfrastructure.TestHost.LocateFrom(AssetFixture.TargetAssetPath, AssetName, TargetFrameworks.NetFramework[0]);
45+
46+
// disableTelemetry: false is essential - it is what forces AppInsightsProvider (and therefore the
47+
// old extension's IngestLoopAsync, where #9710 crashes) to actually be constructed and run.
48+
TestHostResult testHostResult = await testHost.ExecuteAsync(disableTelemetry: false, cancellationToken: TestContext.CancellationToken);
49+
50+
testHostResult.AssertExitCodeIs(ExitCode.Success);
51+
testHostResult.AssertOutputContainsSummary(failed: 0, passed: 1, skipped: 0);
52+
53+
// Guard explicitly against the #9710 failure mode so a regression produces an actionable message
54+
// rather than an opaque native crash exit code.
55+
Assert.IsFalse(
56+
testHostResult.StandardOutput.Contains("MissingMethodException", StringComparison.Ordinal),
57+
$"The test host crashed with a MissingMethodException, which is the #9710 regression.{Environment.NewLine}{testHostResult}");
58+
}
59+
60+
public sealed class TestAssetFixture() : TestAssetFixtureBase()
61+
{
62+
// Oldest supported 2.x extension release. Its netstandard2.0 telemetry asset references the
63+
// platform's internal Polyfills.Polyfill helper, which is what makes it a forward-compat canary.
64+
private const string OldestSupportedExtensionVersion = "2.0.0";
65+
66+
private const string TestCode = """
67+
#file TelemetryForwardCompatibilityTest.csproj
68+
<Project Sdk="Microsoft.NET.Sdk">
69+
<PropertyGroup>
70+
<TargetFrameworks>$TargetFrameworks$</TargetFrameworks>
71+
<ImplicitUsings>enable</ImplicitUsings>
72+
<Nullable>enable</Nullable>
73+
<OutputType>Exe</OutputType>
74+
<PlatformTarget>x64</PlatformTarget>
75+
<LangVersion>preview</LangVersion>
76+
<!-- We provide our own Main, so disable the SDK-generated entry point. -->
77+
<GenerateTestingPlatformEntryPoint>false</GenerateTestingPlatformEntryPoint>
78+
</PropertyGroup>
79+
80+
<ItemGroup>
81+
<!-- Use the locally built (newer) version of the platform. -->
82+
<PackageReference Include="Microsoft.Testing.Platform" Version="$MicrosoftTestingPlatformVersion$" />
83+
84+
<!-- Use the oldest supported 2.x telemetry extension to test forward compatibility. On .NET
85+
Framework this resolves the netstandard2.0 asset that references the platform's internal
86+
Polyfills.Polyfill helper (see #9710). -->
87+
<PackageReference Include="Microsoft.Testing.Extensions.Telemetry" Version="$OldestSupportedExtensionVersion$" />
88+
</ItemGroup>
89+
</Project>
90+
91+
#file Program.cs
92+
using Microsoft.Testing.Extensions;
93+
using Microsoft.Testing.Platform.Builder;
94+
using Microsoft.Testing.Platform.Capabilities.TestFramework;
95+
using Microsoft.Testing.Platform.Extensions.Messages;
96+
using Microsoft.Testing.Platform.Extensions.TestFramework;
97+
using Microsoft.Testing.Platform.Services;
98+
99+
public class Program
100+
{
101+
public static async Task<int> Main(string[] args)
102+
{
103+
ITestApplicationBuilder builder = await TestApplication.CreateBuilderAsync(args);
104+
builder.RegisterTestFramework(
105+
_ => new TestFrameworkCapabilities(),
106+
(_, __) => new DummyTestFramework());
107+
108+
// Registers the AppInsightsProvider from the OLD telemetry extension. Combined with telemetry
109+
// being enabled at runtime, this is what exercises the ingest loop that crashes in #9710.
110+
builder.AddAppInsightsTelemetryProvider();
111+
112+
using ITestApplication app = await builder.BuildAsync();
113+
return await app.RunAsync();
114+
}
115+
}
116+
117+
public class DummyTestFramework : ITestFramework, IDataProducer
118+
{
119+
public string Uid => nameof(DummyTestFramework);
120+
121+
public string Version => "2.0.0";
122+
123+
public string DisplayName => nameof(DummyTestFramework);
124+
125+
public string Description => nameof(DummyTestFramework);
126+
127+
public Type[] DataTypesProduced => new[] { typeof(TestNodeUpdateMessage) };
128+
129+
public Task<bool> IsEnabledAsync() => Task.FromResult(true);
130+
131+
public Task<CreateTestSessionResult> CreateTestSessionAsync(CreateTestSessionContext context)
132+
=> Task.FromResult(new CreateTestSessionResult() { IsSuccess = true });
133+
134+
public Task<CloseTestSessionResult> CloseTestSessionAsync(CloseTestSessionContext context)
135+
=> Task.FromResult(new CloseTestSessionResult() { IsSuccess = true });
136+
137+
public async Task ExecuteRequestAsync(ExecuteRequestContext context)
138+
{
139+
await context.MessageBus.PublishAsync(this, new TestNodeUpdateMessage(context.Request.Session.SessionUid,
140+
new TestNode() { Uid = "0", DisplayName = "TelemetryForwardCompatibilityTest", Properties = new(PassedTestNodeStateProperty.CachedInstance) }));
141+
context.Complete();
142+
}
143+
}
144+
""";
145+
146+
public string TargetAssetPath => GetAssetPath(AssetName);
147+
148+
// This asset references only the platform and telemetry, and provides its own entry point, so
149+
// there is nothing for the MSTest source generator to process.
150+
protected override IReadOnlyList<MetadataMode> SourceGenMetadataModes => [];
151+
152+
public override (string ID, string Name, string Code) GetAssetsToGenerate() => (AssetName, AssetName,
153+
TestCode
154+
// Multi-target so the net462 asset (where #9710 reproduces) exists on Windows while the
155+
// asset still builds on non-Windows (where TargetFrameworks.All omits .NET Framework).
156+
.PatchTargetFrameworks(TargetFrameworks.All)
157+
.PatchCodeWithReplace("$MicrosoftTestingPlatformVersion$", MicrosoftTestingPlatformVersion)
158+
.PatchCodeWithReplace("$OldestSupportedExtensionVersion$", OldestSupportedExtensionVersion));
159+
}
160+
161+
public TestContext TestContext { get; set; }
162+
}

0 commit comments

Comments
 (0)