diff --git a/src/Tasks.UnitTests/CrossTargetingTargetsImportTests.cs b/src/Tasks.UnitTests/CrossTargetingTargetsImportTests.cs new file mode 100644 index 00000000000..025d46c0d61 --- /dev/null +++ b/src/Tasks.UnitTests/CrossTargetingTargetsImportTests.cs @@ -0,0 +1,97 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.IO; + +using Microsoft.Build.Evaluation; +using Microsoft.Build.UnitTests; + +using Shouldly; + +using Xunit; + +namespace Microsoft.Build.Tasks.UnitTests; + +/// +/// Tests for the NuGet restore targets import in Microsoft.Common.CrossTargeting.targets. +/// The parallel import in Microsoft.Common.CurrentVersion.targets is guarded with +/// Exists('$(NuGetRestoreTargets)'), but for years the cross-targeting variant was unguarded +/// — which meant cross-targeting (multi-TFM) projects loaded in non-NuGet contexts (custom build +/// systems, focused unit tests) failed with "Imported project not found" unless the caller +/// manufactured a stub NuGet.targets file. These tests cover the now-guarded import. +/// +public sealed class CrossTargetingTargetsImportTests +{ + private readonly ITestOutputHelper _output; + + public CrossTargetingTargetsImportTests(ITestOutputHelper output) + { + _output = output; + } + + /// + /// When NuGetRestoreTargets points at a file that does not exist, evaluating a project + /// that imports Microsoft.Common.CrossTargeting.targets must succeed (the import is a + /// silent no-op), matching the long-standing behavior of Microsoft.Common.CurrentVersion.targets. + /// + [Fact] + public void ImportSucceedsWhenNuGetRestoreTargetsDoesNotExist() + { + using TestEnvironment env = TestEnvironment.Create(_output); + TransientTestFolder folder = env.CreateFolder(createFolder: true); + string nonExistentNuGetTargets = Path.Combine(folder.Path, "DoesNotExist.NuGet.targets"); + + string projectContents = $""" + + + {nonExistentNuGetTargets} + + + + """.Cleanup(); + + using ProjectFromString projectFromString = new(projectContents); + Project project = projectFromString.Project; + + project.GetPropertyValue("NuGetRestoreTargets").ShouldBe(nonExistentNuGetTargets); + } + + /// + /// When NuGetRestoreTargets points at a real file, the import still happens. This + /// guards against the new Exists() condition accidentally skipping a valid NuGet + /// targets file. The synthetic project does not set IsRestoreTargetsFileLoaded, and + /// nothing in the Microsoft.Common.CrossTargeting.targets import chain sets it before + /// the guarded import, so the canonical guard does not short-circuit here. + /// + [Fact] + public void ImportLoadsNuGetTargetsWhenItExists() + { + using TestEnvironment env = TestEnvironment.Create(_output); + TransientTestFolder folder = env.CreateFolder(createFolder: true); + + const string MarkerTargetName = "_CrossTargetingNuGetStubMarker"; + TransientTestFile nuGetStub = env.CreateFile( + folder, + "NuGet.targets", + $""" + + + + """.Cleanup()); + + string projectContents = $""" + + + {nuGetStub.Path} + + + + """.Cleanup(); + + using ProjectFromString projectFromString = new(projectContents); + Project project = projectFromString.Project; + + project.Targets.ShouldContainKey(MarkerTargetName); + } +} + diff --git a/src/Tasks/Microsoft.Common.CrossTargeting.targets b/src/Tasks/Microsoft.Common.CrossTargeting.targets index 811dcdc42cc..296912f5025 100644 --- a/src/Tasks/Microsoft.Common.CrossTargeting.targets +++ b/src/Tasks/Microsoft.Common.CrossTargeting.targets @@ -201,7 +201,7 @@ Copyright (C) Microsoft Corporation. All rights reserved. $(MSBuildToolsPath)\NuGet.targets - +