From e6d27b1b2e128ca1b0e56e206883cfa7c9283363 Mon Sep 17 00:00:00 2001 From: Jakub Jares Date: Mon, 27 Apr 2026 17:36:20 +0200 Subject: [PATCH 01/11] Fix SCI binding for DTA-like hosts without binding redirects Force .NET Framework product projects to compile against the netstandard2.0 build of System.Collections.Immutable (AssemblyVersion 9.0.0.0) instead of the net462 build (AssemblyVersion 9.0.0.11). SCI 9.0.11 introduced an AV divergence between the two TFMs; the nupkg ships the netstandard2.0 DLL, so compiled metadata must reference 9.0.0.0 for consumers without binding redirects (e.g. Azure DevOps Distributed Test Agent) to avoid FileLoadException. - Directory.Build.targets: ExcludeAssets=compile on SCI PackageReference + explicit Reference to netstandard2.0 DLL for .NET Framework product projects - Extend binding redirect oldVersion to cover 9.0.0.11 - Add DtaLikeHost test asset and acceptance test Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- Directory.Build.targets | 22 ++++ src/datacollector/app.config | 2 +- src/testhost.x86/app.config | 2 +- src/vstest.console/app.config | 2 +- .../DistributedTestAgentScenarioTests.cs | 120 ++++++++++++++++++ .../TestAssets/DtaLikeHost/DtaLikeHost.csproj | 71 +++++++++++ test/TestAssets/DtaLikeHost/Program.cs | 53 ++++++++ 7 files changed, 269 insertions(+), 3 deletions(-) create mode 100644 test/Microsoft.TestPlatform.Acceptance.IntegrationTests/DistributedTestAgentScenarioTests.cs create mode 100644 test/TestAssets/DtaLikeHost/DtaLikeHost.csproj create mode 100644 test/TestAssets/DtaLikeHost/Program.cs diff --git a/Directory.Build.targets b/Directory.Build.targets index 8311c1a9cf..c5d5242a74 100644 --- a/Directory.Build.targets +++ b/Directory.Build.targets @@ -74,4 +74,26 @@ + + + + + $(PkgSystem_Collections_Immutable)\lib\netstandard2.0\System.Collections.Immutable.dll + + + diff --git a/src/datacollector/app.config b/src/datacollector/app.config index 9fb228c9dc..934ad117b6 100644 --- a/src/datacollector/app.config +++ b/src/datacollector/app.config @@ -26,7 +26,7 @@ - + diff --git a/src/testhost.x86/app.config b/src/testhost.x86/app.config index 305c4a5a7d..d54f0815bf 100644 --- a/src/testhost.x86/app.config +++ b/src/testhost.x86/app.config @@ -34,7 +34,7 @@ - + diff --git a/src/vstest.console/app.config b/src/vstest.console/app.config index 36b75a0955..eb6cf8b414 100644 --- a/src/vstest.console/app.config +++ b/src/vstest.console/app.config @@ -26,7 +26,7 @@ - + diff --git a/test/Microsoft.TestPlatform.Acceptance.IntegrationTests/DistributedTestAgentScenarioTests.cs b/test/Microsoft.TestPlatform.Acceptance.IntegrationTests/DistributedTestAgentScenarioTests.cs new file mode 100644 index 0000000000..b73f9e7a64 --- /dev/null +++ b/test/Microsoft.TestPlatform.Acceptance.IntegrationTests/DistributedTestAgentScenarioTests.cs @@ -0,0 +1,120 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.IO; + +using Microsoft.TestPlatform.TestUtilities; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace Microsoft.TestPlatform.AcceptanceTests; + +/// +/// Reproduces the binding-redirect scenario experienced by Azure DevOps' Distributed +/// Test Agent (DTAExecutionHost) and any Visual Studio host that picks up +/// Microsoft.VisualStudio.TestPlatform.Common.dll without the in-box +/// vstest.console.exe.config binding redirects. +/// +/// The test loads Common.dll inside a net472 host that has no binding +/// redirects in its app.config and calls +/// , +/// which triggers FastFilter.Builder and forces +/// System.Collections.Immutable / System.Reflection.Metadata to load. +/// +/// It runs the scenario twice: +/// 1. Against the Microsoft.TestPlatform nupkg's +/// tools/net462/Common7/IDE/Extensions/TestPlatform/ layout (as DTA consumes it). +/// 2. Against the flat layout of the Microsoft.VisualStudio.TestTools.TestPlatform.V2.CLI +/// VSIX (as Visual Studio consumes it). +/// +/// Regression guard: if Common.dll's compiled metadata references for SCI/SRM drift +/// away from the versions we ship next to it, the test fails with the same +/// FileLoadException customers see. Both layouts must stay self-consistent. +/// +[TestClass] +public class DistributedTestAgentScenarioTests : AcceptanceTestBase +{ + [TestMethod] + [TestCategory("Windows-Review")] + public void LoadingCommonDllFromMicrosoftTestPlatformPackageWithoutBindingRedirectsDoesNotThrow() + { + // Nupkg layout: DTA-style consumption of the Microsoft.TestPlatform nupkg. + RunDtaLikeHost(toolsDirOverride: null); + } + + [TestMethod] + [TestCategory("Windows-Review")] + public void LoadingCommonDllFromCliV2VsixLayoutWithoutBindingRedirectsDoesNotThrow() + { + // VSIX layout: flat folder with Common.dll + SCI + SRM at the root, as shipped + // in Microsoft.VisualStudio.TestTools.TestPlatform.V2.CLI.vsix and consumed by + // Visual Studio. The VSIX is unzipped into PublishDirectory by Build.cs. + var extractedVsixDir = Path.Combine( + IntegrationTestEnvironment.PublishDirectory, + Path.GetFileName(IntegrationTestEnvironment.LocalVsixInsertion)); + + Assert.IsTrue( + Directory.Exists(extractedVsixDir), + $"Extracted VSIX directory not found at '{extractedVsixDir}'. " + + "Build.cs is expected to unzip the V2.CLI VSIX before acceptance tests run."); + + Assert.IsTrue( + File.Exists(Path.Combine(extractedVsixDir, "Microsoft.VisualStudio.TestPlatform.Common.dll")), + $"Expected Common.dll at the root of the extracted VSIX ('{extractedVsixDir}')."); + + RunDtaLikeHost(toolsDirOverride: extractedVsixDir); + } + + private void RunDtaLikeHost(string? toolsDirOverride) + { + var projectPath = GetIsolatedTestAsset("DtaLikeHost.csproj", "net472"); + var workingDir = Path.GetDirectoryName(projectPath)!; + + var dotnetPath = GetPatchedDotnetPath(); + + var buildArgs = + $@"build ""{projectPath}"" -c {IntegrationTestEnvironment.BuildConfiguration} " + + $@"/p:PackageVersion={IntegrationTestEnvironment.LatestLocallyBuiltNugetVersion} " + + @"/nodeReuse:false"; + + if (toolsDirOverride is not null) + { + buildArgs += $@" /p:TestPlatformToolsDirOverride=""{toolsDirOverride}"""; + } + + ExecuteApplication(dotnetPath, buildArgs, out var buildOut, out var buildErr, out var buildExit, workingDirectory: workingDir); + + Assert.AreEqual( + 0, + buildExit, + $"dotnet build of DtaLikeHost failed (exit {buildExit}).\nSTDOUT:\n{buildOut}\nSTDERR:\n{buildErr}"); + + var exePath = Path.Combine( + workingDir, + "artifacts", "bin", "TestAssets", "DtaLikeHost", + IntegrationTestEnvironment.BuildConfiguration, + "net472", + "DtaLikeHost.exe"); + + Assert.IsTrue(File.Exists(exePath), $"Expected DtaLikeHost.exe at '{exePath}'."); + + ExecuteApplication(exePath, args: null, out var runOut, out var runErr, out var runExit); + + Assert.AreEqual( + 0, + runExit, + "DtaLikeHost.exe exited non-zero, which means Common.dll's compiled metadata " + + "references for System.Collections.Immutable / System.Reflection.Metadata do " + + "not match the versions shipped next to it. DTA-style hosts (no binding " + + "redirects) will FileLoadException on FastFilter.Builder.\n" + + $"Tools dir: {toolsDirOverride ?? ""}\n" + + $"STDOUT:\n{runOut}\nSTDERR:\n{runErr}"); + + Assert.Contains("OK - no binding exception.", runOut); + } + + private static string GetPatchedDotnetPath() + { + var executable = OSUtils.IsWindows ? "dotnet.exe" : "dotnet"; + return Path.GetFullPath(Path.Combine(IntegrationTestEnvironment.RepoRootDirectory, "artifacts", "tmp", ".dotnet", executable)); + } +} diff --git a/test/TestAssets/DtaLikeHost/DtaLikeHost.csproj b/test/TestAssets/DtaLikeHost/DtaLikeHost.csproj new file mode 100644 index 0000000000..cf11b4e281 --- /dev/null +++ b/test/TestAssets/DtaLikeHost/DtaLikeHost.csproj @@ -0,0 +1,71 @@ + + + + + + DtaLikeHost + net472 + Exe + + false + false + true + + false + false + + + + + + + + + <_TestPlatformToolsDir>$(PkgMicrosoft_TestPlatform)\tools\net462\Common7\IDE\Extensions\TestPlatform + + + + + <_TestPlatformToolsDir>$(TestPlatformToolsDirOverride) + + + + + + $(_TestPlatformToolsDir)\Microsoft.VisualStudio.TestPlatform.Common.dll + true + + + $(_TestPlatformToolsDir)\Microsoft.VisualStudio.TestPlatform.ObjectModel.dll + true + + + + + + + + diff --git a/test/TestAssets/DtaLikeHost/Program.cs b/test/TestAssets/DtaLikeHost/Program.cs new file mode 100644 index 0000000000..7999bed8a5 --- /dev/null +++ b/test/TestAssets/DtaLikeHost/Program.cs @@ -0,0 +1,53 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Reflection; + +using Microsoft.VisualStudio.TestPlatform.Common.Filtering; + +namespace DtaLikeHost; + +internal static class Program +{ + private static int Main() + { + // Report what Common.dll expects and what we ship next to it, so the mismatch + // (or agreement) is visible in the console output regardless of whether the + // CLR actually fails to bind. + var commonAsm = typeof(FilterExpressionWrapper).Assembly; + Console.WriteLine($"Common.dll path: {commonAsm.Location}"); + Console.WriteLine($"Common.dll version: {commonAsm.GetName().Version}"); + foreach (var r in commonAsm.GetReferencedAssemblies()) + { + if (r.Name == "System.Collections.Immutable" || r.Name == "System.Reflection.Metadata") + { + Console.WriteLine($" Common.dll references {r.Name}, Version={r.Version}"); + } + } + + try + { + // A simple equality filter produces a FastFilter, which triggers + // FastFilter.Builder.ctor -> ImmutableDictionary.CreateBuilder(...) + // -> forces the CLR to resolve System.Collections.Immutable at the version + // baked into Common.dll's metadata. + var wrapper = new FilterExpressionWrapper("TestCategory=Foo"); + Console.WriteLine($"FilterExpressionWrapper constructed: FilterString='{wrapper.FilterString}', ParseError='{wrapper.ParseError}'"); + + // Reflect on the private FastFilter field to prove it was actually built. + var fastFilterField = typeof(FilterExpressionWrapper).GetField("FastFilter", BindingFlags.Instance | BindingFlags.NonPublic); + var fastFilter = fastFilterField?.GetValue(wrapper); + Console.WriteLine($"FastFilter built: {fastFilter is not null}"); + } + catch (Exception ex) + { + Console.Error.WriteLine("REPRO HIT: exception constructing FilterExpressionWrapper:"); + Console.Error.WriteLine(ex); + return 1; + } + + Console.WriteLine("OK - no binding exception."); + return 0; + } +} From 5e0b7bf7551810e098f5441ea5201720f9b9652b Mon Sep 17 00:00:00 2001 From: Jakub Jares Date: Tue, 28 Apr 2026 16:32:59 +0200 Subject: [PATCH 02/11] Fix SCI binding failure in DTA hosts without binding redirects Add explicit System.Collections.Immutable PackageReference to CoreUtilities and ObjectModel for non-.NETCoreApp targets (netstandard2.0 + .NETFramework). This ensures all product assemblies compile against SCI 9.0.0.0 (the netstandard2.0 assembly version), matching the SCI DLL shipped in nupkgs. Extend SCI binding redirect oldVersion range to 9.0.0.11 in app.configs to cover the net462 assembly version from SCI 9.0.11. Suppress MSB3277 for .NETCoreApp targets in Directory.Build.targets since the netstandard2.0 SCI 9.0.0.0 reference is harmless on .NET Core (the runtime loads whatever version is available regardless of the reference). Fix MSBuildWarningsAsMessages inheritance in packaging projects that was overwriting parent values instead of appending. Add acceptance test that validates SCI version alignment in nupkg/VSIX layouts using PEReader/MetadataReader. Fixes #15718 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- Directory.Build.targets | 7 ++ eng/expected-nupkg-file-counts.json | 2 +- ...icrosoft.TestPlatform.CoreUtilities.csproj | 1 + .../Microsoft.TestPlatform.ObjectModel.csproj | 1 + src/datacollector/app.config | 2 +- .../Microsoft.TestPlatform.CLI.csproj | 2 +- .../Microsoft.TestPlatform.Portable.csproj | 2 +- .../Microsoft.TestPlatform.csproj | 2 +- src/testhost.x86/app.config | 2 +- src/vstest.console/app.config | 2 +- .../DistributedTestAgentScenarioTests.cs | 100 ++++++++++++++++++ 11 files changed, 116 insertions(+), 7 deletions(-) create mode 100644 test/Microsoft.TestPlatform.Acceptance.IntegrationTests/DistributedTestAgentScenarioTests.cs diff --git a/Directory.Build.targets b/Directory.Build.targets index 8311c1a9cf..d4ec3b2c27 100644 --- a/Directory.Build.targets +++ b/Directory.Build.targets @@ -7,6 +7,13 @@ $(DefineConstants);IS_VSTEST_REPO + + $(MSBuildWarningsAsMessages);MSB3277 diff --git a/eng/expected-nupkg-file-counts.json b/eng/expected-nupkg-file-counts.json index f39ca1ea27..1a4a62385c 100644 --- a/eng/expected-nupkg-file-counts.json +++ b/eng/expected-nupkg-file-counts.json @@ -12,5 +12,5 @@ "Microsoft.TestPlatform.Portable": 608, "Microsoft.TestPlatform.TestHost": 65, "Microsoft.TestPlatform.TranslationLayer": 175, - "Microsoft.VisualStudio.TestTools.TestPlatform.V2.CLI": 389 + "Microsoft.VisualStudio.TestTools.TestPlatform.V2.CLI": 484 } diff --git a/src/Microsoft.TestPlatform.CoreUtilities/Microsoft.TestPlatform.CoreUtilities.csproj b/src/Microsoft.TestPlatform.CoreUtilities/Microsoft.TestPlatform.CoreUtilities.csproj index 866225a77c..83631f94ea 100644 --- a/src/Microsoft.TestPlatform.CoreUtilities/Microsoft.TestPlatform.CoreUtilities.csproj +++ b/src/Microsoft.TestPlatform.CoreUtilities/Microsoft.TestPlatform.CoreUtilities.csproj @@ -18,6 +18,7 @@ + diff --git a/src/Microsoft.TestPlatform.ObjectModel/Microsoft.TestPlatform.ObjectModel.csproj b/src/Microsoft.TestPlatform.ObjectModel/Microsoft.TestPlatform.ObjectModel.csproj index 33872067fb..71028d9e3a 100644 --- a/src/Microsoft.TestPlatform.ObjectModel/Microsoft.TestPlatform.ObjectModel.csproj +++ b/src/Microsoft.TestPlatform.ObjectModel/Microsoft.TestPlatform.ObjectModel.csproj @@ -35,6 +35,7 @@ + diff --git a/src/datacollector/app.config b/src/datacollector/app.config index 9fb228c9dc..934ad117b6 100644 --- a/src/datacollector/app.config +++ b/src/datacollector/app.config @@ -26,7 +26,7 @@ - + diff --git a/src/package/Microsoft.TestPlatform.CLI/Microsoft.TestPlatform.CLI.csproj b/src/package/Microsoft.TestPlatform.CLI/Microsoft.TestPlatform.CLI.csproj index 1497c93107..bfec73c5a5 100644 --- a/src/package/Microsoft.TestPlatform.CLI/Microsoft.TestPlatform.CLI.csproj +++ b/src/package/Microsoft.TestPlatform.CLI/Microsoft.TestPlatform.CLI.csproj @@ -37,7 +37,7 @@ Sometimes NU1702 is not suppressed correctly, so force reducing severity of the warning. See https://github.com/NuGet/Home/issues/9147 --> - NU1702 + $(MSBuildWarningsAsMessages);NU1702 diff --git a/src/package/Microsoft.TestPlatform.Portable/Microsoft.TestPlatform.Portable.csproj b/src/package/Microsoft.TestPlatform.Portable/Microsoft.TestPlatform.Portable.csproj index 8583c5cbff..1e86120d0e 100644 --- a/src/package/Microsoft.TestPlatform.Portable/Microsoft.TestPlatform.Portable.csproj +++ b/src/package/Microsoft.TestPlatform.Portable/Microsoft.TestPlatform.Portable.csproj @@ -12,7 +12,7 @@ Sometimes NU1702 is not suppressed correctly, so force reducing severity of the warning. See https://github.com/NuGet/Home/issues/9147 --> - NU1702 + $(MSBuildWarningsAsMessages);NU1702 diff --git a/src/package/Microsoft.TestPlatform/Microsoft.TestPlatform.csproj b/src/package/Microsoft.TestPlatform/Microsoft.TestPlatform.csproj index 9bed4c9ad3..15141365dc 100644 --- a/src/package/Microsoft.TestPlatform/Microsoft.TestPlatform.csproj +++ b/src/package/Microsoft.TestPlatform/Microsoft.TestPlatform.csproj @@ -11,7 +11,7 @@ Sometimes NU1702 is not suppressed correctly, so force reducing severity of the warning. See https://github.com/NuGet/Home/issues/9147 --> - NU1702;NETSDK1023 + $(MSBuildWarningsAsMessages);NU1702;NETSDK1023 diff --git a/src/testhost.x86/app.config b/src/testhost.x86/app.config index 305c4a5a7d..d54f0815bf 100644 --- a/src/testhost.x86/app.config +++ b/src/testhost.x86/app.config @@ -34,7 +34,7 @@ - + diff --git a/src/vstest.console/app.config b/src/vstest.console/app.config index 36b75a0955..eb6cf8b414 100644 --- a/src/vstest.console/app.config +++ b/src/vstest.console/app.config @@ -26,7 +26,7 @@ - + diff --git a/test/Microsoft.TestPlatform.Acceptance.IntegrationTests/DistributedTestAgentScenarioTests.cs b/test/Microsoft.TestPlatform.Acceptance.IntegrationTests/DistributedTestAgentScenarioTests.cs new file mode 100644 index 0000000000..a54858b130 --- /dev/null +++ b/test/Microsoft.TestPlatform.Acceptance.IntegrationTests/DistributedTestAgentScenarioTests.cs @@ -0,0 +1,100 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.IO; +using System.Linq; +using System.Reflection.Metadata; +using System.Reflection.PortableExecutable; + +using Microsoft.TestPlatform.TestUtilities; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace Microsoft.TestPlatform.AcceptanceTests; + +/// +/// Tests that the DTA (Distributed Test Agent) scenario works: a .NET Framework host +/// loads Microsoft.TestPlatform.Common.dll without binding redirects. The shipped +/// System.Collections.Immutable DLL must match the assembly version referenced by +/// the compiled product assemblies. +/// +[TestClass] +public class DistributedTestAgentScenarioTests : AcceptanceTestBase +{ + /// + /// Validates that the Microsoft.TestPlatform nupkg layout (used by DTA/VSTest task) + /// contains a System.Collections.Immutable whose assembly version matches what + /// Common.dll was compiled against. + /// + [TestMethod] + public void NupkgLayout_CommonDll_CanLoadSciWithoutBindingRedirects() + { + var testPlatformDir = Path.Combine( + IntegrationTestEnvironment.PublishDirectory, + $"Microsoft.TestPlatform.{IntegrationTestEnvironment.LatestLocallyBuiltNugetVersion}.nupkg", + "tools", "net462", "Common7", "IDE", "Extensions", "TestPlatform"); + + ValidateDtaScenario(testPlatformDir, "nupkg"); + } + + /// + /// Validates that the V2.CLI VSIX layout also has matching SCI assembly versions. + /// + [TestMethod] + public void VsixLayout_CommonDll_CanLoadSciWithoutBindingRedirects() + { + var vsixDir = Path.GetDirectoryName(IntegrationTestEnvironment.LocalVsixInsertion); + if (vsixDir is null || !Directory.Exists(vsixDir)) + { + Assert.Inconclusive("VSIX directory not found. Build with -pack to produce it."); + } + + var vsixExtractDir = Path.Combine(IntegrationTestEnvironment.ArtifactsTempDirectory, "vsix-extracted"); + if (!Directory.Exists(vsixExtractDir)) + { + System.IO.Compression.ZipFile.ExtractToDirectory(IntegrationTestEnvironment.LocalVsixInsertion, vsixExtractDir); + } + + ValidateDtaScenario(vsixExtractDir, "VSIX"); + } + + private static void ValidateDtaScenario(string testPlatformDir, string layoutName) + { + Assert.IsTrue(Directory.Exists(testPlatformDir), $"{layoutName} directory not found: {testPlatformDir}"); + + var commonDll = Path.Combine(testPlatformDir, "Microsoft.TestPlatform.Common.dll"); + var sciDll = Path.Combine(testPlatformDir, "System.Collections.Immutable.dll"); + + Assert.IsTrue(File.Exists(commonDll), $"Common.dll not found in {layoutName} layout: {commonDll}"); + Assert.IsTrue(File.Exists(sciDll), $"System.Collections.Immutable.dll not found in {layoutName} layout: {sciDll}"); + + var sciVersion = System.Reflection.AssemblyName.GetAssemblyName(sciDll).Version; + var sciRefVersion = GetReferencedAssemblyVersion(commonDll, "System.Collections.Immutable"); + + Assert.IsNotNull(sciRefVersion, $"Common.dll in {layoutName} layout does not reference System.Collections.Immutable"); + + // For DTA scenario (no binding redirects), these MUST match exactly. + Assert.AreEqual( + sciRefVersion, + sciVersion, + $"{layoutName} layout: Common.dll references SCI {sciRefVersion} but shipped DLL is {sciVersion}. " + + $"DTA hosts without binding redirects will fail with FileLoadException."); + } + + private static Version? GetReferencedAssemblyVersion(string assemblyPath, string referenceName) + { + using var stream = File.OpenRead(assemblyPath); + using var peReader = new PEReader(stream); + var mdReader = peReader.GetMetadataReader(); + foreach (var handle in mdReader.AssemblyReferences) + { + var asmRef = mdReader.GetAssemblyReference(handle); + if (mdReader.GetString(asmRef.Name) == referenceName) + { + return asmRef.Version; + } + } + + return null; + } +} From a99062731dd30e0501f7065f485a13a78b71ca4d Mon Sep 17 00:00:00 2001 From: Jakub Jares Date: Tue, 28 Apr 2026 16:34:02 +0200 Subject: [PATCH 03/11] Remove unused DtaLikeHost test asset --- .../TestAssets/DtaLikeHost/DtaLikeHost.csproj | 71 ------------------- test/TestAssets/DtaLikeHost/Program.cs | 53 -------------- 2 files changed, 124 deletions(-) delete mode 100644 test/TestAssets/DtaLikeHost/DtaLikeHost.csproj delete mode 100644 test/TestAssets/DtaLikeHost/Program.cs diff --git a/test/TestAssets/DtaLikeHost/DtaLikeHost.csproj b/test/TestAssets/DtaLikeHost/DtaLikeHost.csproj deleted file mode 100644 index cf11b4e281..0000000000 --- a/test/TestAssets/DtaLikeHost/DtaLikeHost.csproj +++ /dev/null @@ -1,71 +0,0 @@ - - - - - - DtaLikeHost - net472 - Exe - - false - false - true - - false - false - - - - - - - - - <_TestPlatformToolsDir>$(PkgMicrosoft_TestPlatform)\tools\net462\Common7\IDE\Extensions\TestPlatform - - - - - <_TestPlatformToolsDir>$(TestPlatformToolsDirOverride) - - - - - - $(_TestPlatformToolsDir)\Microsoft.VisualStudio.TestPlatform.Common.dll - true - - - $(_TestPlatformToolsDir)\Microsoft.VisualStudio.TestPlatform.ObjectModel.dll - true - - - - - - - - diff --git a/test/TestAssets/DtaLikeHost/Program.cs b/test/TestAssets/DtaLikeHost/Program.cs deleted file mode 100644 index 7999bed8a5..0000000000 --- a/test/TestAssets/DtaLikeHost/Program.cs +++ /dev/null @@ -1,53 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT license. See LICENSE file in the project root for full license information. - -using System; -using System.Reflection; - -using Microsoft.VisualStudio.TestPlatform.Common.Filtering; - -namespace DtaLikeHost; - -internal static class Program -{ - private static int Main() - { - // Report what Common.dll expects and what we ship next to it, so the mismatch - // (or agreement) is visible in the console output regardless of whether the - // CLR actually fails to bind. - var commonAsm = typeof(FilterExpressionWrapper).Assembly; - Console.WriteLine($"Common.dll path: {commonAsm.Location}"); - Console.WriteLine($"Common.dll version: {commonAsm.GetName().Version}"); - foreach (var r in commonAsm.GetReferencedAssemblies()) - { - if (r.Name == "System.Collections.Immutable" || r.Name == "System.Reflection.Metadata") - { - Console.WriteLine($" Common.dll references {r.Name}, Version={r.Version}"); - } - } - - try - { - // A simple equality filter produces a FastFilter, which triggers - // FastFilter.Builder.ctor -> ImmutableDictionary.CreateBuilder(...) - // -> forces the CLR to resolve System.Collections.Immutable at the version - // baked into Common.dll's metadata. - var wrapper = new FilterExpressionWrapper("TestCategory=Foo"); - Console.WriteLine($"FilterExpressionWrapper constructed: FilterString='{wrapper.FilterString}', ParseError='{wrapper.ParseError}'"); - - // Reflect on the private FastFilter field to prove it was actually built. - var fastFilterField = typeof(FilterExpressionWrapper).GetField("FastFilter", BindingFlags.Instance | BindingFlags.NonPublic); - var fastFilter = fastFilterField?.GetValue(wrapper); - Console.WriteLine($"FastFilter built: {fastFilter is not null}"); - } - catch (Exception ex) - { - Console.Error.WriteLine("REPRO HIT: exception constructing FilterExpressionWrapper:"); - Console.Error.WriteLine(ex); - return 1; - } - - Console.WriteLine("OK - no binding exception."); - return 0; - } -} From 3553871e5191ff42da140d62aacc5828d5cd8096 Mon Sep 17 00:00:00 2001 From: Jakub Jares Date: Tue, 28 Apr 2026 16:53:00 +0200 Subject: [PATCH 04/11] Remove old ExcludeAssets approach from Directory.Build.targets The merge with the old PR branch brought back the ExcludeAssets/HintPath workaround that was replaced by the simpler PackageReference approach. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- Directory.Build.targets | 22 ---------------------- 1 file changed, 22 deletions(-) diff --git a/Directory.Build.targets b/Directory.Build.targets index 6606479abb..d4ec3b2c27 100644 --- a/Directory.Build.targets +++ b/Directory.Build.targets @@ -81,26 +81,4 @@ - - - - - $(PkgSystem_Collections_Immutable)\lib\netstandard2.0\System.Collections.Immutable.dll - - - From 3a83e0c415cfc864c04f5eff4d4ff184f9527699 Mon Sep 17 00:00:00 2001 From: Jakub Jares Date: Tue, 28 Apr 2026 17:18:15 +0200 Subject: [PATCH 05/11] Fix stale nupkg file count for V2.CLI (389, not 484) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- eng/expected-nupkg-file-counts.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/eng/expected-nupkg-file-counts.json b/eng/expected-nupkg-file-counts.json index 1a4a62385c..f39ca1ea27 100644 --- a/eng/expected-nupkg-file-counts.json +++ b/eng/expected-nupkg-file-counts.json @@ -12,5 +12,5 @@ "Microsoft.TestPlatform.Portable": 608, "Microsoft.TestPlatform.TestHost": 65, "Microsoft.TestPlatform.TranslationLayer": 175, - "Microsoft.VisualStudio.TestTools.TestPlatform.V2.CLI": 484 + "Microsoft.VisualStudio.TestTools.TestPlatform.V2.CLI": 389 } From 7eaedb1fd0f508ad06c72a6356bb9fed59317522 Mon Sep 17 00:00:00 2001 From: Jakub Jares Date: Tue, 28 Apr 2026 17:42:37 +0200 Subject: [PATCH 06/11] Fix SCI condition to .NETFramework only The previous condition '!= .NETCoreApp' included netstandard2.0, which made netstandard2.0 assemblies reference SCI 9.0.0.0. This breaks on .NET 8 where the shared framework only has SCI 8.0.0.0. Change to '== .NETFramework' so only net462 builds get the explicit SCI reference (covered by binding redirects), while netstandard2.0 keeps SCI 8.0.0.0 from the SRM transitive dependency. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Microsoft.TestPlatform.CoreUtilities.csproj | 2 +- .../Microsoft.TestPlatform.ObjectModel.csproj | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Microsoft.TestPlatform.CoreUtilities/Microsoft.TestPlatform.CoreUtilities.csproj b/src/Microsoft.TestPlatform.CoreUtilities/Microsoft.TestPlatform.CoreUtilities.csproj index 83631f94ea..41188a01ea 100644 --- a/src/Microsoft.TestPlatform.CoreUtilities/Microsoft.TestPlatform.CoreUtilities.csproj +++ b/src/Microsoft.TestPlatform.CoreUtilities/Microsoft.TestPlatform.CoreUtilities.csproj @@ -18,7 +18,7 @@ - + diff --git a/src/Microsoft.TestPlatform.ObjectModel/Microsoft.TestPlatform.ObjectModel.csproj b/src/Microsoft.TestPlatform.ObjectModel/Microsoft.TestPlatform.ObjectModel.csproj index 71028d9e3a..f27cff3cf6 100644 --- a/src/Microsoft.TestPlatform.ObjectModel/Microsoft.TestPlatform.ObjectModel.csproj +++ b/src/Microsoft.TestPlatform.ObjectModel/Microsoft.TestPlatform.ObjectModel.csproj @@ -35,7 +35,7 @@ - + From ecbf3732b16473fe3a129156e682c5963bd4fe2a Mon Sep 17 00:00:00 2001 From: Jakub Jares Date: Tue, 28 Apr 2026 18:04:04 +0200 Subject: [PATCH 07/11] Remove DTA acceptance test The DTA scenario (loading SCI without binding redirects) cannot be fully fixed with SCI 9.0.11 due to assembly version divergence between net462 (9.0.0.11) and netstandard2.0 (9.0.0.0). This will be tracked in a separate issue. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- eng/expected-nupkg-file-counts.json | 2 +- .../DistributedTestAgentScenarioTests.cs | 100 ------------------ 2 files changed, 1 insertion(+), 101 deletions(-) delete mode 100644 test/Microsoft.TestPlatform.Acceptance.IntegrationTests/DistributedTestAgentScenarioTests.cs diff --git a/eng/expected-nupkg-file-counts.json b/eng/expected-nupkg-file-counts.json index f39ca1ea27..1a4a62385c 100644 --- a/eng/expected-nupkg-file-counts.json +++ b/eng/expected-nupkg-file-counts.json @@ -12,5 +12,5 @@ "Microsoft.TestPlatform.Portable": 608, "Microsoft.TestPlatform.TestHost": 65, "Microsoft.TestPlatform.TranslationLayer": 175, - "Microsoft.VisualStudio.TestTools.TestPlatform.V2.CLI": 389 + "Microsoft.VisualStudio.TestTools.TestPlatform.V2.CLI": 484 } diff --git a/test/Microsoft.TestPlatform.Acceptance.IntegrationTests/DistributedTestAgentScenarioTests.cs b/test/Microsoft.TestPlatform.Acceptance.IntegrationTests/DistributedTestAgentScenarioTests.cs deleted file mode 100644 index a54858b130..0000000000 --- a/test/Microsoft.TestPlatform.Acceptance.IntegrationTests/DistributedTestAgentScenarioTests.cs +++ /dev/null @@ -1,100 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT license. See LICENSE file in the project root for full license information. - -using System; -using System.IO; -using System.Linq; -using System.Reflection.Metadata; -using System.Reflection.PortableExecutable; - -using Microsoft.TestPlatform.TestUtilities; -using Microsoft.VisualStudio.TestTools.UnitTesting; - -namespace Microsoft.TestPlatform.AcceptanceTests; - -/// -/// Tests that the DTA (Distributed Test Agent) scenario works: a .NET Framework host -/// loads Microsoft.TestPlatform.Common.dll without binding redirects. The shipped -/// System.Collections.Immutable DLL must match the assembly version referenced by -/// the compiled product assemblies. -/// -[TestClass] -public class DistributedTestAgentScenarioTests : AcceptanceTestBase -{ - /// - /// Validates that the Microsoft.TestPlatform nupkg layout (used by DTA/VSTest task) - /// contains a System.Collections.Immutable whose assembly version matches what - /// Common.dll was compiled against. - /// - [TestMethod] - public void NupkgLayout_CommonDll_CanLoadSciWithoutBindingRedirects() - { - var testPlatformDir = Path.Combine( - IntegrationTestEnvironment.PublishDirectory, - $"Microsoft.TestPlatform.{IntegrationTestEnvironment.LatestLocallyBuiltNugetVersion}.nupkg", - "tools", "net462", "Common7", "IDE", "Extensions", "TestPlatform"); - - ValidateDtaScenario(testPlatformDir, "nupkg"); - } - - /// - /// Validates that the V2.CLI VSIX layout also has matching SCI assembly versions. - /// - [TestMethod] - public void VsixLayout_CommonDll_CanLoadSciWithoutBindingRedirects() - { - var vsixDir = Path.GetDirectoryName(IntegrationTestEnvironment.LocalVsixInsertion); - if (vsixDir is null || !Directory.Exists(vsixDir)) - { - Assert.Inconclusive("VSIX directory not found. Build with -pack to produce it."); - } - - var vsixExtractDir = Path.Combine(IntegrationTestEnvironment.ArtifactsTempDirectory, "vsix-extracted"); - if (!Directory.Exists(vsixExtractDir)) - { - System.IO.Compression.ZipFile.ExtractToDirectory(IntegrationTestEnvironment.LocalVsixInsertion, vsixExtractDir); - } - - ValidateDtaScenario(vsixExtractDir, "VSIX"); - } - - private static void ValidateDtaScenario(string testPlatformDir, string layoutName) - { - Assert.IsTrue(Directory.Exists(testPlatformDir), $"{layoutName} directory not found: {testPlatformDir}"); - - var commonDll = Path.Combine(testPlatformDir, "Microsoft.TestPlatform.Common.dll"); - var sciDll = Path.Combine(testPlatformDir, "System.Collections.Immutable.dll"); - - Assert.IsTrue(File.Exists(commonDll), $"Common.dll not found in {layoutName} layout: {commonDll}"); - Assert.IsTrue(File.Exists(sciDll), $"System.Collections.Immutable.dll not found in {layoutName} layout: {sciDll}"); - - var sciVersion = System.Reflection.AssemblyName.GetAssemblyName(sciDll).Version; - var sciRefVersion = GetReferencedAssemblyVersion(commonDll, "System.Collections.Immutable"); - - Assert.IsNotNull(sciRefVersion, $"Common.dll in {layoutName} layout does not reference System.Collections.Immutable"); - - // For DTA scenario (no binding redirects), these MUST match exactly. - Assert.AreEqual( - sciRefVersion, - sciVersion, - $"{layoutName} layout: Common.dll references SCI {sciRefVersion} but shipped DLL is {sciVersion}. " + - $"DTA hosts without binding redirects will fail with FileLoadException."); - } - - private static Version? GetReferencedAssemblyVersion(string assemblyPath, string referenceName) - { - using var stream = File.OpenRead(assemblyPath); - using var peReader = new PEReader(stream); - var mdReader = peReader.GetMetadataReader(); - foreach (var handle in mdReader.AssemblyReferences) - { - var asmRef = mdReader.GetAssemblyReference(handle); - if (mdReader.GetString(asmRef.Name) == referenceName) - { - return asmRef.Version; - } - } - - return null; - } -} From d9dc902e3343666468c362d0c974129d6b0de973 Mon Sep 17 00:00:00 2001 From: Jakub Jares Date: Tue, 28 Apr 2026 18:25:42 +0200 Subject: [PATCH 08/11] Bump SCI to 10.0.0 to fix AV divergence SCI 9.0.11 has different assembly versions for net462 (9.0.0.11) vs netstandard2.0 (9.0.0.0). DTA hosts without binding redirects need exact version match. SCI 10.0.0 has AV 10.0.0.0 for all TFMs. - Bump SystemCollectionsImmutablePackageVersion to 10.0.0 - Update binding redirects to 10.0.0.0 - Restore DTA acceptance test with correct DLL name Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- eng/Version.Details.props | 2 +- src/datacollector/app.config | 2 +- src/testhost.x86/app.config | 2 +- src/vstest.console/app.config | 2 +- .../DistributedTestAgentScenarioTests.cs | 101 ++++++++++++++++++ 5 files changed, 105 insertions(+), 4 deletions(-) create mode 100644 test/Microsoft.TestPlatform.Acceptance.IntegrationTests/DistributedTestAgentScenarioTests.cs diff --git a/eng/Version.Details.props b/eng/Version.Details.props index 97bb6dc3b2..7d3f641a07 100644 --- a/eng/Version.Details.props +++ b/eng/Version.Details.props @@ -13,7 +13,7 @@ This file should be imported by eng/Versions.props 0.2.0-preview.26226.111 6.0.2 - 9.0.11 + 10.0.0 1.1.0-beta2-19575-01 1.1.0-beta2-19575-01 diff --git a/src/datacollector/app.config b/src/datacollector/app.config index 934ad117b6..bc2401d517 100644 --- a/src/datacollector/app.config +++ b/src/datacollector/app.config @@ -26,7 +26,7 @@ - + diff --git a/src/testhost.x86/app.config b/src/testhost.x86/app.config index d54f0815bf..220811c23c 100644 --- a/src/testhost.x86/app.config +++ b/src/testhost.x86/app.config @@ -34,7 +34,7 @@ - + diff --git a/src/vstest.console/app.config b/src/vstest.console/app.config index eb6cf8b414..d37e11367d 100644 --- a/src/vstest.console/app.config +++ b/src/vstest.console/app.config @@ -26,7 +26,7 @@ - + diff --git a/test/Microsoft.TestPlatform.Acceptance.IntegrationTests/DistributedTestAgentScenarioTests.cs b/test/Microsoft.TestPlatform.Acceptance.IntegrationTests/DistributedTestAgentScenarioTests.cs new file mode 100644 index 0000000000..972a6d6f2c --- /dev/null +++ b/test/Microsoft.TestPlatform.Acceptance.IntegrationTests/DistributedTestAgentScenarioTests.cs @@ -0,0 +1,101 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.IO; +using System.Linq; +using System.Reflection.Metadata; +using System.Reflection.PortableExecutable; + +using Microsoft.TestPlatform.TestUtilities; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace Microsoft.TestPlatform.AcceptanceTests; + +/// +/// Tests that the DTA (Distributed Test Agent) scenario works: a .NET Framework host +/// loads Microsoft.VisualStudio.TestPlatform.Common.dll without binding redirects. The shipped +/// System.Collections.Immutable DLL must match the assembly version referenced by +/// the compiled product assemblies. +/// +[TestClass] +public class DistributedTestAgentScenarioTests : AcceptanceTestBase +{ + /// + /// Validates that the Microsoft.TestPlatform nupkg layout (used by DTA/VSTest task) + /// contains a System.Collections.Immutable whose assembly version matches what + /// Common.dll was compiled against. + /// + [TestMethod] + public void NupkgLayout_CommonDll_CanLoadSciWithoutBindingRedirects() + { + var testPlatformDir = Path.Combine( + IntegrationTestEnvironment.PublishDirectory, + $"Microsoft.TestPlatform.{IntegrationTestEnvironment.LatestLocallyBuiltNugetVersion}.nupkg", + "tools", "net462", "Common7", "IDE", "Extensions", "TestPlatform"); + + ValidateDtaScenario(testPlatformDir, "nupkg"); + } + + /// + /// Validates that the V2.CLI VSIX layout also has matching SCI assembly versions. + /// + [TestMethod] + public void VsixLayout_CommonDll_CanLoadSciWithoutBindingRedirects() + { + var vsixDir = Path.GetDirectoryName(IntegrationTestEnvironment.LocalVsixInsertion); + if (vsixDir is null || !Directory.Exists(vsixDir)) + { + Assert.Inconclusive("VSIX directory not found. Build with -pack to produce it."); + } + + var vsixExtractDir = Path.Combine(IntegrationTestEnvironment.ArtifactsTempDirectory, "vsix-extracted"); + if (!Directory.Exists(vsixExtractDir)) + { + System.IO.Compression.ZipFile.ExtractToDirectory(IntegrationTestEnvironment.LocalVsixInsertion, vsixExtractDir); + } + + ValidateDtaScenario(vsixExtractDir, "VSIX"); + } + + private static void ValidateDtaScenario(string testPlatformDir, string layoutName) + { + Assert.IsTrue(Directory.Exists(testPlatformDir), $"{layoutName} directory not found: {testPlatformDir}"); + + var commonDll = Path.Combine(testPlatformDir, "Microsoft.VisualStudio.TestPlatform.Common.dll"); + var sciDll = Path.Combine(testPlatformDir, "System.Collections.Immutable.dll"); + + Assert.IsTrue(File.Exists(commonDll), $"Common.dll not found in {layoutName} layout: {commonDll}"); + Assert.IsTrue(File.Exists(sciDll), $"System.Collections.Immutable.dll not found in {layoutName} layout: {sciDll}"); + + var sciVersion = System.Reflection.AssemblyName.GetAssemblyName(sciDll).Version; + var sciRefVersion = GetReferencedAssemblyVersion(commonDll, "System.Collections.Immutable"); + + Assert.IsNotNull(sciRefVersion, $"Common.dll in {layoutName} layout does not reference System.Collections.Immutable"); + + // For DTA scenario (no binding redirects), these MUST match exactly. + Assert.AreEqual( + sciRefVersion, + sciVersion, + $"{layoutName} layout: Common.dll references SCI {sciRefVersion} but shipped DLL is {sciVersion}. " + + $"DTA hosts without binding redirects will fail with FileLoadException."); + } + + private static Version? GetReferencedAssemblyVersion(string assemblyPath, string referenceName) + { + using var stream = File.OpenRead(assemblyPath); + using var peReader = new PEReader(stream); + var mdReader = peReader.GetMetadataReader(); + foreach (var handle in mdReader.AssemblyReferences) + { + var asmRef = mdReader.GetAssemblyReference(handle); + if (mdReader.GetString(asmRef.Name) == referenceName) + { + return asmRef.Version; + } + } + + return null; + } +} + From d1e38c9ec6a4c15d53604e7d6bd603b20bcc17a8 Mon Sep 17 00:00:00 2001 From: Jakub Jares Date: Tue, 28 Apr 2026 21:42:38 +0200 Subject: [PATCH 09/11] Fix System.Memory and Unsafe binding redirect versions for Release builds verify-binding-redirects auto-corrected: - System.Memory: 4.0.1.2 -> 4.0.5.0 (Release build ships newer AV) - System.Runtime.CompilerServices.Unsafe: 6.0.0.0 -> 6.0.3.0 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/datacollector/app.config | 4 ++-- src/testhost.x86/app.config | 4 ++-- src/vstest.console/app.config | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/datacollector/app.config b/src/datacollector/app.config index bc2401d517..78c8e9aed5 100644 --- a/src/datacollector/app.config +++ b/src/datacollector/app.config @@ -22,7 +22,7 @@ - + @@ -34,7 +34,7 @@ - + diff --git a/src/testhost.x86/app.config b/src/testhost.x86/app.config index 220811c23c..07bcbe9ae4 100644 --- a/src/testhost.x86/app.config +++ b/src/testhost.x86/app.config @@ -30,7 +30,7 @@ - + @@ -42,7 +42,7 @@ - + diff --git a/src/vstest.console/app.config b/src/vstest.console/app.config index d37e11367d..c8275a0834 100644 --- a/src/vstest.console/app.config +++ b/src/vstest.console/app.config @@ -22,7 +22,7 @@ - + @@ -35,7 +35,7 @@ - + From 8fa4f6fa74606e8d5616d7586d9ad10a862f54e7 Mon Sep 17 00:00:00 2001 From: Jakub Jares Date: Tue, 28 Apr 2026 22:10:37 +0200 Subject: [PATCH 10/11] Update V2.CLI nupkg file count to 389 (clean build) Previous count of 484 was from a dirty build with stale artifacts. Clean Release build produces 389 files, matching CI. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- eng/expected-nupkg-file-counts.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/eng/expected-nupkg-file-counts.json b/eng/expected-nupkg-file-counts.json index 1a4a62385c..f39ca1ea27 100644 --- a/eng/expected-nupkg-file-counts.json +++ b/eng/expected-nupkg-file-counts.json @@ -12,5 +12,5 @@ "Microsoft.TestPlatform.Portable": 608, "Microsoft.TestPlatform.TestHost": 65, "Microsoft.TestPlatform.TranslationLayer": 175, - "Microsoft.VisualStudio.TestTools.TestPlatform.V2.CLI": 484 + "Microsoft.VisualStudio.TestTools.TestPlatform.V2.CLI": 389 } From 5eba36dbffff94fe553f8304792a0b9a0015d502 Mon Sep 17 00:00:00 2001 From: Jakub Jares Date: Wed, 29 Apr 2026 09:44:56 +0200 Subject: [PATCH 11/11] Add System.Buffers binding redirect to all app.configs System.Memory 4.6.3 (transitive from SCI 10.0.0) depends on System.Buffers. In Release builds, System.Buffers.dll has AV 4.0.5.0, but MSTest adapter loads against 4.0.3.0. Without a redirect, the CLR cannot resolve the mismatch. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/datacollector/app.config | 5 +++++ src/testhost.x86/app.config | 5 +++++ src/vstest.console/app.config | 5 +++++ 3 files changed, 15 insertions(+) diff --git a/src/datacollector/app.config b/src/datacollector/app.config index 78c8e9aed5..2744e280a0 100644 --- a/src/datacollector/app.config +++ b/src/datacollector/app.config @@ -36,6 +36,11 @@ + + + + + diff --git a/src/testhost.x86/app.config b/src/testhost.x86/app.config index 07bcbe9ae4..c6e47644e1 100644 --- a/src/testhost.x86/app.config +++ b/src/testhost.x86/app.config @@ -44,6 +44,11 @@ + + + + + diff --git a/src/vstest.console/app.config b/src/vstest.console/app.config index c8275a0834..f2bfcfed8c 100644 --- a/src/vstest.console/app.config +++ b/src/vstest.console/app.config @@ -38,6 +38,11 @@ + + + + +