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 @@ -308,7 +308,7 @@ private async Task ExecuteTestsInSourceAsync(IEnumerable<TestCase> tests, IRunCo
int parallelWorkers = sourceSettings.Workers;
ExecutionScope parallelScope = sourceSettings.Scope;
TestCase[] testsToRun = [.. tests.Where(t => MatchTestFilter(filterExpression, t, _testMethodFilter))];
UnitTestElement[] unitTestElements = [.. testsToRun.Select(e => e.ToUnitTestElement(source))];
UnitTestElement[] unitTestElements = [.. testsToRun.Select(e => e.ToUnitTestElementWithUpdatedSource(source))];
// Create an instance of a type defined in adapter so that adapter gets loaded in the child app domain
var testRunner = (UnitTestRunner)isolationHost.CreateInstanceForType(
typeof(UnitTestRunner),
Expand Down Expand Up @@ -455,7 +455,7 @@ private async Task ExecuteTestsWithTestRunnerAsync(
}

hasAnyRunnableTests = true;
var unitTestElement = currentTest.ToUnitTestElement(source);
UnitTestElement unitTestElement = currentTest.ToUnitTestElementWithUpdatedSource(source);

testExecutionRecorder.RecordStart(currentTest);

Expand Down Expand Up @@ -517,7 +517,7 @@ private async Task ExecuteTestsWithTestRunnerAsync(
}

Trait trait = currentTest.Traits.First(t => t.Name == EngineConstants.FixturesTestTrait);
var unitTestElement = currentTest.ToUnitTestElement(source);
UnitTestElement unitTestElement = currentTest.ToUnitTestElementWithUpdatedSource(source);
FixtureTestResult fixtureTestResult = testRunner.GetFixtureTestResult(unitTestElement.TestMethod, trait.Value);

if (fixtureTestResult.IsExecuted)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -736,6 +736,9 @@ private MethodInfo GetMethodInfoForTestMethod(TestMethod testMethod, TestClassIn
// testMethod.MethodInfo can be null if 'TestMethod' instance crossed app domain boundaries.
// This happens on .NET Framework when app domain is enabled, and the MethodInfo is calculated and set during discovery.
// Then, execution will cause TestMethod to cross to a different app domain, and MethodInfo will be null.
// In addition, it also happens when deployment items are used and app domain is disabled.
// We explicitly set it to null in this case because the original MethodInfo calculated during discovery cannot be used because
// it points to the type loaded from the assembly in bin instead of from deployment directory.
methodBase = testMethod.MethodInfo ?? ManagedNameHelper.GetMethod(testClassInfo.Parent.Assembly, testMethod.ManagedTypeName!, testMethod.ManagedMethodName!);
}
catch (InvalidManagedNameException)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,11 +68,19 @@ internal static string GetTestName(this TestCase testCase, string? testClassName
/// <param name="testCase"> The test case. </param>
/// <param name="source"> The source. If deployed this is the full path of the source in the deployment directory. </param>
/// <returns> The converted <see cref="UnitTestElement"/>. </returns>
internal static UnitTestElement ToUnitTestElement(this TestCase testCase, string source)
internal static UnitTestElement ToUnitTestElementWithUpdatedSource(this TestCase testCase, string source)
{
if (testCase.LocalExtensionData is UnitTestElement unitTestElement)
{
return unitTestElement;
// If the requested source is different, clone it with updated source.
// This can happen when there are deployment items in tests.
// In this case, the source would be the path of the deployment directory.
// If we don't return UnitTestElement with the correct path to deployment directory, we will
// end up trying to load the test assembly twice in the same appdomain, once with the default context and once in a LoadFrom context.
// See https://github.com/microsoft/testfx/issues/6713
return unitTestElement.TestMethod.AssemblyName != source
? unitTestElement.CloneWithUpdatedSource(source)
: unitTestElement;
}

string? testClassName = testCase.GetPropertyValue(EngineConstants.TestClassNameProperty) as string;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -165,4 +165,12 @@ public string? DeclaringClassFullName
internal string DisplayName { get; set; }

internal TestMethod Clone() => (TestMethod)MemberwiseClone();

internal TestMethod CloneWithUpdatedSource(string source)
{
var clone = (TestMethod)MemberwiseClone();
AssemblyName = source;
MethodInfo = null;
return clone;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,13 @@ internal UnitTestElement Clone()
return clone;
}

internal UnitTestElement CloneWithUpdatedSource(string source)
{
var clone = (UnitTestElement)MemberwiseClone();
clone.TestMethod = TestMethod.CloneWithUpdatedSource(source);
return clone;
}

/// <summary>
/// Convert the UnitTestElement instance to an Object Model testCase instance.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
// 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.Acceptance.IntegrationTests;
using Microsoft.Testing.Platform.Acceptance.IntegrationTests.Helpers;
using Microsoft.Testing.Platform.Helpers;

namespace MSTest.Acceptance.IntegrationTests;

[TestClass]
public class DeploymentItemTests : AcceptanceTestBase<DeploymentItemTests.TestAssetFixture>
{
private const string AssetName = nameof(DeploymentItemTests);

[TestMethod]
[OSCondition(OperatingSystems.Windows)]
[DataRow("AppDomainDisabled.runsettings", IgnoreMessage = "https://github.com/microsoft/testfx/issues/6738")]
[DataRow("AppDomainEnabled.runsettings")]
public async Task AssemblyIsLoadedOnceFromDeploymentDirectory(string runsettings)
{
var testHost = TestHost.LocateFrom(AssetFixture.TargetAssetPath, AssetName, TargetFrameworks.NetFramework[0]);
TestHostResult testHostResult = await testHost.ExecuteAsync($"--settings {runsettings}", cancellationToken: TestContext.CancellationToken);
testHostResult.AssertOutputContainsSummary(failed: 0, passed: 1, skipped: 0);
testHostResult.AssertExitCodeIs(ExitCodes.Success);
}

public sealed class TestAssetFixture() : TestAssetFixtureBase(AcceptanceFixture.NuGetGlobalPackagesFolder)
{
private const string Sources = """
#file DeploymentItemTests.csproj
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFrameworks>$TargetFrameworks$</TargetFrameworks>
<EnableMSTestRunner>true</EnableMSTestRunner>
<OutputType>Exe</OutputType>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="MSTest.TestFramework" Version="$MSTestVersion$" />
<PackageReference Include="MSTest.TestAdapter" Version="$MSTestVersion$" />
</ItemGroup>

<ItemGroup>
<None Update="TestDeploymentItem.xml">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>

<ItemGroup>
<None Update="AppDomainEnabled.runsettings">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="AppDomainDisabled.runsettings">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>

</Project>

#file TestDeploymentItem.xml
<?xml version="1.0" encoding="utf-8" ?>
<Root />

#file AppDomainEnabled.runsettings
<?xml version="1.0" encoding="utf-8" ?>
<RunSettings>
<RunConfiguration>
<DisableAppDomain>false</DisableAppDomain>
</RunConfiguration>
</RunSettings>

#file AppDomainDisabled.runsettings
<?xml version="1.0" encoding="utf-8" ?>
<RunSettings>
<RunConfiguration>
<DisableAppDomain>true</DisableAppDomain>
</RunConfiguration>
</RunSettings>

#file TestClass1.cs
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Microsoft.VisualStudio.TestTools.UnitTesting;

[TestClass]
public sealed class Test1
{
public TestContext TestContext { get; set; }

[TestMethod]
[DeploymentItem("TestDeploymentItem.xml")]
public void TestMethod1()
{
var asm = Assert.ContainsSingle(AppDomain.CurrentDomain.GetAssemblies().Where(a => a.GetName().Name == "DeploymentItemTests"));
var path = asm.Location;
Assert.AreEqual(TestContext.DeploymentDirectory, Path.GetDirectoryName(path));
}
}
""";

public string TargetAssetPath => GetAssetPath(AssetName);

public override IEnumerable<(string ID, string Name, string Code)> GetAssetsToGenerate()
{
yield return (AssetName, AssetName,
Sources
.PatchTargetFrameworks(TargetFrameworks.NetFramework[0])
.PatchCodeWithReplace("$MSTestVersion$", MSTestVersion));
}
}

public TestContext TestContext { get; set; }
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.Extensions;
using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.ObjectModel;
using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices;
using Microsoft.VisualStudio.TestPlatform.ObjectModel;

Expand All @@ -23,7 +24,7 @@ public void ToUnitTestElementShouldReturnUnitTestElementWithFieldsSet()
testCase.SetPropertyValue(EngineConstants.TestCategoryProperty, testCategories);
testCase.SetPropertyValue(EngineConstants.TestClassNameProperty, "DummyClassName");

var resultUnitTestElement = testCase.ToUnitTestElement(testCase.Source);
UnitTestElement resultUnitTestElement = testCase.ToUnitTestElementWithUpdatedSource(testCase.Source);

Verify(resultUnitTestElement.Priority == 2);
Verify(testCategories == resultUnitTestElement.TestCategory);
Expand All @@ -38,7 +39,7 @@ public void ToUnitTestElementForTestCaseWithNoPropertiesShouldReturnUnitTestElem
TestCase testCase = new("DummyClass.DummyMethod", new("DummyUri", UriKind.Relative), Assembly.GetCallingAssembly().FullName!);
testCase.SetPropertyValue(EngineConstants.TestClassNameProperty, "DummyClassName");

var resultUnitTestElement = testCase.ToUnitTestElement(testCase.Source);
UnitTestElement resultUnitTestElement = testCase.ToUnitTestElementWithUpdatedSource(testCase.Source);

// These are set for testCase by default by ObjectModel.
Verify(resultUnitTestElement.Priority == 0);
Expand All @@ -51,7 +52,7 @@ public void ToUnitTestElementShouldAddDeclaringClassNameToTestElementWhenAvailab
testCase.SetPropertyValue(EngineConstants.TestClassNameProperty, "DummyClassName");
testCase.SetPropertyValue(EngineConstants.DeclaringClassNameProperty, "DummyDeclaringClassName");

var resultUnitTestElement = testCase.ToUnitTestElement(testCase.Source);
UnitTestElement resultUnitTestElement = testCase.ToUnitTestElementWithUpdatedSource(testCase.Source);

Verify(resultUnitTestElement.TestMethod.FullClassName == "DummyClassName");
Verify(resultUnitTestElement.TestMethod.DeclaringClassFullName == "DummyDeclaringClassName");
Expand Down