Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ Copyright (c) .NET Foundation. All rights reserved.
<PropertyGroup Condition="'$(UseCurrentRuntimeIdentifier)' == ''">
<UseCurrentRuntimeIdentifier Condition="
'$(RuntimeIdentifier)' == '' and
'$(_IsExecutable)' == 'true' and '$(IsTestProject)' != 'true' and
'$(_IsExecutable)' == 'true' and
'$(IsRidAgnostic)' != 'true' and
'$(PackAsTool)' != true and
(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ Copyright (c) .NET Foundation. All rights reserved.
<!-- Set the IsRidAgnostic property if this project should NOT accept global RuntimeIdentifier and SelfContained
property values from referencing projects. -->
<PropertyGroup Condition="'$(IsRidAgnostic)' == ''">
<IsRidAgnostic Condition="('$(_IsExecutable)' == 'true' And '$(IsTestProject)' != 'true') Or
<IsRidAgnostic Condition="('$(_IsExecutable)' == 'true') Or
'$(RuntimeIdentifier)' != '' Or
'$(RuntimeIdentifiers)' != ''">false</IsRidAgnostic>
<IsRidAgnostic Condition="'$(IsRidAgnostic)' == ''">true</IsRidAgnostic>
Expand Down Expand Up @@ -1292,27 +1292,28 @@ Copyright (c) .NET Foundation. All rights reserved.
<ShouldBeValidatedAsExecutableReference>false</ShouldBeValidatedAsExecutableReference>
</PropertyGroup>

<PropertyGroup Condition="'$(IsTestProject)' == 'true' And '$(ValidateExecutableReferencesMatchSelfContained)' == ''">
<!-- Don't generate an error if a test project references a self-contained Exe. Test projects
use an OutputType of Exe but will usually call APIs in a referenced Exe rather than try
to run it. -->
<ValidateExecutableReferencesMatchSelfContained>false</ValidateExecutableReferencesMatchSelfContained>
</PropertyGroup>

<PropertyGroup Condition="'$(IsTestProject)' == 'true'">
<!-- Don't generate an error if an Exe project references a test project. -->
<ShouldBeValidatedAsExecutableReference>false</ShouldBeValidatedAsExecutableReference>
</PropertyGroup>


<UsingTask TaskName="ValidateExecutableReferences" AssemblyFile="$(MicrosoftNETBuildTasksAssembly)" />

<PropertyGroup>
<_UseAttributeForTargetFrameworkInfoPropertyNames Condition="$([MSBuild]::VersionGreaterThanOrEquals($(MSBuildVersion), '17.0'))">true</_UseAttributeForTargetFrameworkInfoPropertyNames>
</PropertyGroup>

<Target Name="_CalculateIsVSTest">
<PropertyGroup>
<_IsVSTest Condition="'$(IsTestProject)' == 'true' and '$(IsTestingPlatformApplication)' != 'true'">true</_IsVSTest>
<_IsVSTest Condition="'$(_IsVSTest)' == ''">false</_IsVSTest>

<!-- Don't generate an error if an Exe project references a test project. -->
<ShouldBeValidatedAsExecutableReference Condition="'$(_IsVSTest)' == 'true'">false</ShouldBeValidatedAsExecutableReference>

<!-- Don't generate an error if a test project references a self-contained Exe. Test projects
use an OutputType of Exe but will usually call APIs in a referenced Exe rather than try
to run it. -->
<ValidateExecutableReferencesMatchSelfContained Condition="'$(_IsVSTest)' == 'true' And '$(ValidateExecutableReferencesMatchSelfContained)' == ''">false</ValidateExecutableReferencesMatchSelfContained>
</PropertyGroup>
</Target>

<Target Name="ValidateExecutableReferences"
AfterTargets="_GetProjectReferenceTargetFrameworkProperties"
Condition="'$(ValidateExecutableReferencesMatchSelfContained)' != 'false'">

<ValidateExecutableReferences
Expand All @@ -1322,6 +1323,12 @@ Copyright (c) .NET Foundation. All rights reserved.
UseAttributeForTargetFrameworkInfoPropertyNames="$(_UseAttributeForTargetFrameworkInfoPropertyNames)"/>
</Target>

<Target Name="ValidateExecutableReferencesCore"
DependsOnTargets="_CalculateIsVSTest;ValidateExecutableReferences"
AfterTargets="_GetProjectReferenceTargetFrameworkProperties">

</Target>

<!--
============================================================
Command Line Configuration Validation
Expand Down
169 changes: 166 additions & 3 deletions test/Microsoft.NET.Build.Tests/ReferenceExeTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -323,9 +323,10 @@ public void ReferencedExeCanRunWhenPublishedWithTrimming(bool referenceExeInCode
}

[RequiresMSBuildVersionTheory("17.0.0.32901")]
[InlineData("xunit")]
[InlineData("mstest")]
public void TestProjectCanReferenceExe(string testTemplateName)
[CombinatorialData]
public void TestProjectCanReferenceExe(
[CombinatorialValues("xunit", "mstest")] string testTemplateName,
bool setSelfContainedProperty)
{
var testConsoleProject = new TestProject("ConsoleApp")
{
Expand All @@ -334,6 +335,11 @@ public void TestProjectCanReferenceExe(string testTemplateName)
RuntimeIdentifier = EnvironmentInfo.GetCompatibleRid()
};

if (setSelfContainedProperty)
{
testConsoleProject.SelfContained = "true";
}

var testAsset = _testAssetsManager.CreateTestProject(testConsoleProject, identifier: testTemplateName);

var testProjectDirectory = Path.Combine(testAsset.TestRoot, "TestProject");
Expand All @@ -359,6 +365,123 @@ public void TestProjectCanReferenceExe(string testTemplateName)

}

[Theory]
[CombinatorialData]
public void SelfContainedExecutableCannotBeReferencedByNonSelfContainedMTPTestProject(bool setIsTestingPlatformApplicationEarly)
{
// The setup of this test is as follows:
// ConsoleApp is a self-contained executable project.
// MTPTestProject is an executable test project that references ConsoleApp.
// Building MTPTestProject should fail because it references a self-contained executable project.
// A self-contained executable cannot be referenced by a non self-contained executable.
var testConsoleProjectSelfContained = new TestProject("ConsoleApp")
{
IsExe = true,
TargetFrameworks = ToolsetInfo.CurrentTargetFramework,
SelfContained = "true",
};

var testAssetSelfContained = _testAssetsManager.CreateTestProject(testConsoleProjectSelfContained);

var mtpNotSelfContained = new TestProject("MTPTestProject")
{
TargetFrameworks = ToolsetInfo.CurrentTargetFramework,
IsExe = true,
IsTestProject = true,
};

if (setIsTestingPlatformApplicationEarly)
{
mtpNotSelfContained.IsTestingPlatformApplication = true;
}

var testAssetMTP = _testAssetsManager.CreateTestProject(mtpNotSelfContained);

var mtpProjectDirectory = Path.Combine(testAssetMTP.Path, "MTPTestProject");
Assert.True(Directory.Exists(mtpProjectDirectory), $"Expected directory {mtpProjectDirectory} to exist.");
Assert.True(File.Exists(Path.Combine(mtpProjectDirectory, "MTPTestProject.csproj")), $"Expected file MTPTestProject.csproj to exist in {mtpProjectDirectory}.");

if (!setIsTestingPlatformApplicationEarly)
{
File.WriteAllText(Path.Combine(mtpProjectDirectory, "Directory.Build.targets"), """
<Project>
<PropertyGroup>
<IsTestingPlatformApplication>true</IsTestingPlatformApplication>
</PropertyGroup>
</Project>
""");
}

new DotnetCommand(Log, "add", "reference", Path.Combine(testAssetSelfContained.Path, testConsoleProjectSelfContained.Name))
.WithWorkingDirectory(mtpProjectDirectory)
.Execute()
.Should()
.Pass();

var result = new BuildCommand(Log, mtpProjectDirectory).Execute();
result.Should().HaveStdOutContaining("NETSDK1151").And.ExitWith(1);
}

[Theory]
[CombinatorialData]
public void MTPNonSelfContainedExecutableCannotBeReferencedBySelfContained(bool setIsTestingPlatformApplicationEarly)
{
// The setup of this test is as follows:
// ConsoleApp is a self-contained executable project, which references a non-self-contained MTP executable test project.
// Building ConsoleApp should fail because it references a non-self-contained MTP executable project.
// A non self-contained executable cannot be referenced by a self-contained executable.
var testConsoleProjectSelfContained = new TestProject("ConsoleApp")
{
IsExe = true,
TargetFrameworks = ToolsetInfo.CurrentTargetFramework,
SelfContained = "true",
};

var testAssetSelfContained = _testAssetsManager.CreateTestProject(testConsoleProjectSelfContained);

var mtpNotSelfContained = new TestProject("MTPTestProject")
{
TargetFrameworks = ToolsetInfo.CurrentTargetFramework,
IsExe = true,
IsTestProject = true,
};

if (setIsTestingPlatformApplicationEarly)
{
mtpNotSelfContained.IsTestingPlatformApplication = true;
}

var testAssetMTP = _testAssetsManager.CreateTestProject(mtpNotSelfContained);

var mtpProjectDirectory = Path.Combine(testAssetMTP.Path, mtpNotSelfContained.Name);
Assert.True(Directory.Exists(mtpProjectDirectory), $"Expected directory {mtpProjectDirectory} to exist.");
Assert.True(File.Exists(Path.Combine(mtpProjectDirectory, "MTPTestProject.csproj")), $"Expected file MTPTestProject.csproj to exist in {mtpProjectDirectory}.");

if (!setIsTestingPlatformApplicationEarly)
{
File.WriteAllText(Path.Combine(mtpProjectDirectory, "Directory.Build.targets"), """
<Project>
<PropertyGroup>
<IsTestingPlatformApplication>true</IsTestingPlatformApplication>
</PropertyGroup>
</Project>
""");
}

var consoleAppDirectory = Path.Combine(testAssetSelfContained.Path, testConsoleProjectSelfContained.Name);
Assert.True(Directory.Exists(consoleAppDirectory), $"Expected directory {consoleAppDirectory} to exist.");
Assert.True(File.Exists(Path.Combine(consoleAppDirectory, "ConsoleApp.csproj")), $"Expected file ConsoleApp.csproj to exist in {consoleAppDirectory}.");

new DotnetCommand(Log, "add", "reference", Path.Combine(testAssetMTP.Path, mtpNotSelfContained.Name))
.WithWorkingDirectory(consoleAppDirectory)
.Execute()
.Should()
.Pass();

var result = new BuildCommand(Log, consoleAppDirectory).Execute();
result.Should().HaveStdOutContaining("NETSDK1150").And.ExitWith(1);
}

[RequiresMSBuildVersionTheory("17.0.0.32901")]
[InlineData("xunit")]
[InlineData("mstest")]
Expand Down Expand Up @@ -396,5 +519,45 @@ public void ExeProjectCanReferenceTestProject(string testTemplateName)
.Should()
.Pass();
}

[Theory]
[CombinatorialData]
public void MTPCanBeBuiltAsSelfContained(bool setIsTestingPlatformApplicationEarly)
{
var mtpSelfContained = new TestProject("MTPTestProject")
{
TargetFrameworks = ToolsetInfo.CurrentTargetFramework,
IsExe = true,
IsTestProject = true,
SelfContained = "true",
};

if (setIsTestingPlatformApplicationEarly)
{
mtpSelfContained.IsTestingPlatformApplication = true;
}

var testAssetMTP = _testAssetsManager.CreateTestProject(mtpSelfContained);

var mtpProjectDirectory = Path.Combine(testAssetMTP.Path, mtpSelfContained.Name);
Assert.True(Directory.Exists(mtpProjectDirectory), $"Expected directory {mtpProjectDirectory} to exist.");
Assert.True(File.Exists(Path.Combine(mtpProjectDirectory, "MTPTestProject.csproj")), $"Expected file MTPTestProject.csproj to exist in {mtpProjectDirectory}.");

if (!setIsTestingPlatformApplicationEarly)
{
File.WriteAllText(Path.Combine(mtpProjectDirectory, "Directory.Build.targets"), """
<Project>
<PropertyGroup>
<IsTestingPlatformApplication>true</IsTestingPlatformApplication>
</PropertyGroup>
</Project>
""");
}

new BuildCommand(Log, mtpProjectDirectory)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In this case, you might want to also check that the self-contained app correctly runs and runs tests. That will take a bit more time, so I'm not sure if it's a good idea or not.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is nothing special here I think as there is no special casing around MTP. If a typical console app runs correctly as self-contained, then this case shouldn't be different at all.

We can follow-up and add an extra test though, maybe even try to do it with both dotnet run and dotnet test, just to be more confident.

.Execute()
.Should()
.Pass();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,10 @@ public TestProject([CallerMemberName] string? name = null)
/// </summary>
public bool IsWinExe { get; set; }

public bool IsTestingPlatformApplication { get; set; }

public bool IsTestProject { get; set; }


public string? ProjectSdk { get; set; }

Expand Down Expand Up @@ -293,6 +297,16 @@ internal void Create(TestAsset targetTestAsset, string testProjectsSourceFolder)
propertyGroup?.Element(ns + "OutputType")?.SetValue("WinExe");
}

if (IsTestProject)
{
propertyGroup?.Add(new XElement(ns + "IsTestProject", "true"));
}

if (IsTestingPlatformApplication)
{
propertyGroup?.Add(new XElement(ns + "IsTestingPlatformApplication", "true"));
}

if (SelfContained != "")
{
propertyGroup?.Add(new XElement(ns + "SelfContained", string.Equals(SelfContained, "true", StringComparison.OrdinalIgnoreCase) ? "true" : "false"));
Expand Down
Loading