Skip to content

Commit

Permalink
Up-to-date check supports TargetPath metadata
Browse files Browse the repository at this point in the history
Source items which participate in `CopyToOutputDirectory` may specify `TargetPath` metadata to control their location relative to the output directory.

Historically, `Link` metadata has been used to control this, and that was supported here. Now, we support both `TargetPath` and `Link`.
  • Loading branch information
drewnoakes committed Aug 12, 2021
1 parent e8dca67 commit 4d45beb
Show file tree
Hide file tree
Showing 7 changed files with 199 additions and 7 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,18 @@
</StringProperty.DataSource>
</StringProperty>

<StringProperty Name="TargetPath"
Visible="false">
<StringProperty.DataSource>
<DataSource PersistenceStyle="Attribute"
SourceOfDefaultValue="AfterContext" />
</StringProperty.DataSource>
<StringProperty.Metadata>
<NameValuePair Name="DoNotCopyAcrossProjects"
Value="true" />
</StringProperty.Metadata>
</StringProperty>

<BoolProperty Name="Visible"
Visible="false" />

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,18 @@
</StringProperty.DataSource>
</StringProperty>

<StringProperty Name="TargetPath"
Visible="false">
<StringProperty.DataSource>
<DataSource PersistenceStyle="Attribute"
SourceOfDefaultValue="AfterContext" />
</StringProperty.DataSource>
<StringProperty.Metadata>
<NameValuePair Name="DoNotCopyAcrossProjects"
Value="true" />
</StringProperty.Metadata>
</StringProperty>

<BoolProperty Name="Visible"
Visible="false" />

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,18 @@
</StringProperty.DataSource>
</StringProperty>

<StringProperty Name="TargetPath"
Visible="false">
<StringProperty.DataSource>
<DataSource PersistenceStyle="Attribute"
SourceOfDefaultValue="AfterContext" />
</StringProperty.DataSource>
<StringProperty.Metadata>
<NameValuePair Name="DoNotCopyAcrossProjects"
Value="true" />
</StringProperty.Metadata>
</StringProperty>

<BoolProperty Name="Visible"
Visible="false" />

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,9 @@ internal sealed class UpToDateCheckImplicitConfiguredInput
public string? MSBuildProjectFullPath { get; }

public string? MSBuildProjectDirectory { get; }

public string? CopyUpToDateMarkerItem { get; }

public string? OutputRelativeOrFullPath { get; }

/// <summary>
Expand Down Expand Up @@ -525,7 +525,34 @@ static BuildUpToDateCheck.CopyType GetCopyType(IImmutableDictionary<string, stri

static string? GetTargetPath(IImmutableDictionary<string, string> itemMetadata)
{
return itemMetadata.TryGetValue(BuildUpToDateCheck.Link, out string link) ? link : null;
// "Link" is an optional path and file name under which the item should be copied.
// It allows a source file to be moved to a different relative path, or to be renamed.
//
// From the perspective of the FUTD check, it is only relevant on CopyToOutputDirectory items.
//
// Two properties can provide this feature: "Link" and "TargetPath".
//
// If specified, "TargetPath" metadata controls the path of the target file, relative to the output
// folder.
//
// "Link" controls the location under the project in Solution Explorer where the item appears.
// If "TargetPath" is not specified, then "Link" can also serve the role of "TargetPath".
//
// If both are specified, we only use "TargetPath". The use case for specifying both is wanting
// to control the location of the item in Solution Explorer, as well as in the output directory.
// The former is not relevant to us here.

if (itemMetadata.TryGetValue(None.TargetPathProperty, out string? targetPath) && !string.IsNullOrWhiteSpace(targetPath))
{
return targetPath;
}

if (itemMetadata.TryGetValue(None.LinkProperty, out string link) && !string.IsNullOrWhiteSpace(link))
{
return link;
}

return null;
}

static ImmutableDictionary<string, ImmutableDictionary<string, ImmutableArray<string>>> BuildItemsByKindBySetName(IProjectChangeDescription projectChangeDescription, string kindPropertyName, string setPropertyName)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
using System.IO;
using System.Linq;
using System.Xml.Linq;
using System.Xml.XPath;
using Xunit;

namespace Microsoft.VisualStudio.ProjectSystem.Rules
Expand Down Expand Up @@ -35,12 +36,20 @@ public void FileRulesShouldMatchNone(string ruleName, string fullPath)

string noneFile = Path.Combine(fullPath, "..", "None.xaml");

XElement none = LoadXamlRule(noneFile);
XElement none = LoadXamlRule(noneFile, out var namespaceManager);
XElement rule = LoadXamlRule(fullPath);

// First fix up the Name as we know they'll differ.
rule.Attribute("Name").Value = "None";

if (ruleName is "Compile" or "EditorConfigFiles")
{
// Remove the "TargetPath" element for these types
var targetPathElement = none.XPathSelectElement(@"/msb:Rule/msb:StringProperty[@Name=""TargetPath""]", namespaceManager);
Assert.NotNull(targetPathElement);
targetPathElement!.Remove();
}

AssertXmlEqual(none, rule);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,16 @@ private protected static IProjectRuleSnapshotModel ItemWithMetadata(string itemS
};
}

private protected static IProjectRuleSnapshotModel ItemWithMetadata(string itemSpec, params (string MetadataName, string MetadataValue)[] metadata)
{
return new IProjectRuleSnapshotModel
{
Items = ImmutableStringDictionary<IImmutableDictionary<string, string>>.EmptyOrdinal.Add(
itemSpec,
metadata.ToImmutableDictionary(pair => pair.MetadataName, pair => pair.MetadataValue, StringComparer.Ordinal))
};
}

private protected static IProjectRuleSnapshotModel ItemsWithMetadata(params (string itemSpec, string metadataName, string metadataValue)[] items)
{
return new IProjectRuleSnapshotModel
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1051,7 +1051,7 @@ public async Task IsUpToDateAsync_False_CopiedOutputFileDestinationDoesNotExist(
}

[Fact]
public async Task IsUpToDateAsync_False_CopyToOutputDirectorySourceIsNewerThanDestination()
public async Task IsUpToDateAsync_False_CopyToOutputDirectory_SourceIsNewerThanDestination()
{
var sourceSnapshot = new Dictionary<string, IProjectRuleSnapshotModel>
{
Expand Down Expand Up @@ -1087,7 +1087,117 @@ public async Task IsUpToDateAsync_False_CopyToOutputDirectorySourceIsNewerThanDe
}

[Fact]
public async Task IsUpToDateAsync_False_CopyToOutputDirectorySourceDoesNotExist()
public async Task IsUpToDateAsync_False_CopyToOutputDirectory_SourceIsNewerThanDestination_TargetPath()
{
var sourceSnapshot = new Dictionary<string, IProjectRuleSnapshotModel>
{
[Content.SchemaName] = ItemWithMetadata("Item1", ("CopyToOutputDirectory", "PreserveNewest"), ("TargetPath", "TargetPath"))
};

var destinationPath = @"NewProjectDirectory\NewOutputPath\TargetPath";
var sourcePath = @"C:\Dev\Solution\Project\Item1";

var itemChangeTime = DateTime.UtcNow.AddMinutes(-4);
var lastCheckTime = DateTime.UtcNow.AddMinutes(-3);
var destinationTime = DateTime.UtcNow.AddMinutes(-2);
var sourceTime = DateTime.UtcNow.AddMinutes(-1);

await SetupAsync(
sourceSnapshot: sourceSnapshot,
lastCheckTimeAtUtc: lastCheckTime,
lastItemsChangedAtUtc: itemChangeTime);

_fileSystem.AddFile(destinationPath, destinationTime);
_fileSystem.AddFile(sourcePath, sourceTime);

await AssertNotUpToDateAsync(
new[]
{
"No build outputs defined.",
$"Checking PreserveNewest file '{sourcePath}':",
$" Source {sourceTime.ToLocalTime()}: '{sourcePath}'.",
$" Destination {destinationTime.ToLocalTime()}: '{destinationPath}'.",
$"PreserveNewest source '{sourcePath}' is newer than destination '{destinationPath}', not up to date."
},
"CopyToOutputDirectory");
}

[Fact]
public async Task IsUpToDateAsync_False_CopyToOutputDirectory_SourceIsNewerThanDestination_Link()
{
var sourceSnapshot = new Dictionary<string, IProjectRuleSnapshotModel>
{
[Content.SchemaName] = ItemWithMetadata("Item1", ("CopyToOutputDirectory", "PreserveNewest"), ("Link", "LinkPath"))
};

var destinationPath = @"NewProjectDirectory\NewOutputPath\LinkPath";
var sourcePath = @"C:\Dev\Solution\Project\Item1";

var itemChangeTime = DateTime.UtcNow.AddMinutes(-4);
var lastCheckTime = DateTime.UtcNow.AddMinutes(-3);
var destinationTime = DateTime.UtcNow.AddMinutes(-2);
var sourceTime = DateTime.UtcNow.AddMinutes(-1);

await SetupAsync(
sourceSnapshot: sourceSnapshot,
lastCheckTimeAtUtc: lastCheckTime,
lastItemsChangedAtUtc: itemChangeTime);

_fileSystem.AddFile(destinationPath, destinationTime);
_fileSystem.AddFile(sourcePath, sourceTime);

await AssertNotUpToDateAsync(
new[]
{
"No build outputs defined.",
$"Checking PreserveNewest file '{sourcePath}':",
$" Source {sourceTime.ToLocalTime()}: '{sourcePath}'.",
$" Destination {destinationTime.ToLocalTime()}: '{destinationPath}'.",
$"PreserveNewest source '{sourcePath}' is newer than destination '{destinationPath}', not up to date."
},
"CopyToOutputDirectory");
}

[Fact]
public async Task IsUpToDateAsync_False_CopyToOutputDirectory_SourceIsNewerThanDestination_TargetPathAndLink()
{
// When both "Link" and "TargetPath" are present, "TargetPath" takes precedence

var sourceSnapshot = new Dictionary<string, IProjectRuleSnapshotModel>
{
[Content.SchemaName] = ItemWithMetadata("Item1", ("CopyToOutputDirectory", "PreserveNewest"), ("Link", "LinkPath"), ("TargetPath", "TargetPath"))
};

var destinationPath = @"NewProjectDirectory\NewOutputPath\TargetPath";
var sourcePath = @"C:\Dev\Solution\Project\Item1";

var itemChangeTime = DateTime.UtcNow.AddMinutes(-4);
var lastCheckTime = DateTime.UtcNow.AddMinutes(-3);
var destinationTime = DateTime.UtcNow.AddMinutes(-2);
var sourceTime = DateTime.UtcNow.AddMinutes(-1);

await SetupAsync(
sourceSnapshot: sourceSnapshot,
lastCheckTimeAtUtc: lastCheckTime,
lastItemsChangedAtUtc: itemChangeTime);

_fileSystem.AddFile(destinationPath, destinationTime);
_fileSystem.AddFile(sourcePath, sourceTime);

await AssertNotUpToDateAsync(
new[]
{
"No build outputs defined.",
$"Checking PreserveNewest file '{sourcePath}':",
$" Source {sourceTime.ToLocalTime()}: '{sourcePath}'.",
$" Destination {destinationTime.ToLocalTime()}: '{destinationPath}'.",
$"PreserveNewest source '{sourcePath}' is newer than destination '{destinationPath}', not up to date."
},
"CopyToOutputDirectory");
}

[Fact]
public async Task IsUpToDateAsync_False_CopyToOutputDirectory_SourceDoesNotExist()
{
var sourceSnapshot = new Dictionary<string, IProjectRuleSnapshotModel>
{
Expand Down Expand Up @@ -1119,7 +1229,7 @@ public async Task IsUpToDateAsync_False_CopyToOutputDirectorySourceDoesNotExist(
}

[Fact]
public async Task IsUpToDateAsync_False_CopyToOutputDirectoryDestinationDoesNotExist()
public async Task IsUpToDateAsync_False_CopyToOutputDirectory_DestinationDoesNotExist()
{
var sourceSnapshot = new Dictionary<string, IProjectRuleSnapshotModel>
{
Expand Down

0 comments on commit 4d45beb

Please sign in to comment.