|
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Xml;
using Microsoft.Build.Construction;
using Microsoft.Build.Evaluation;
using Microsoft.Build.Execution;
using Microsoft.Build.Shared;
using Xunit;
#nullable disable
namespace Microsoft.Build.UnitTests.OM.Definition
{
/// <summary>
/// Tests for editing through the definition model
/// </summary>
public class DefinitionEditing_Tests
{
public delegate void SetupProject(Project p);
public static IEnumerable<object[]> ItemElementsThatRequireSplitting
{
get
{
// explode on semicolon separated literals
yield return new object[]
{
@"<Project ToolsVersion=""msbuilddefaulttoolsversion"" xmlns=""msbuildnamespace"">
<ItemGroup>
<i Include=""i1;i2"">
{0}
</i>
</ItemGroup>
</Project>",
0, // operate on first item
null // project setup that should be performed after the project is loaded. Used to simulate items coming from globs
};
// explode on item coming from property with one item
yield return new object[]
{
@"<Project ToolsVersion=""msbuilddefaulttoolsversion"" xmlns=""msbuildnamespace"">
<PropertyGroup>
<p>i1</p>
</PropertyGroup>
<ItemGroup>
<i Include=""$(p)"" >
{0}
</i>
</ItemGroup>
</Project>",
0,
null
};
// explode on item coming from property with multiple items
yield return new object[]
{
@"<Project ToolsVersion=""msbuilddefaulttoolsversion"" xmlns=""msbuildnamespace"">
<PropertyGroup>
<p>i1;i2</p>
</PropertyGroup>
<ItemGroup>
<i Include=""$(p)"" >
{0}
</i>
</ItemGroup>
</Project>",
0,
null
};
// explode on item coming from item reference with one item
yield return new object[]
{
@"<Project ToolsVersion=""msbuilddefaulttoolsversion"" xmlns=""msbuildnamespace"">
<ItemGroup>
<a Include=""i1"" />
<i Include=""@(a)"" >
{0}
</i>
</ItemGroup>
</Project>",
1, // operate on the second item. It is produced by the second item element
null
};
// explode on item coming from item reference with multiple items
yield return new object[]
{
@"<Project ToolsVersion=""msbuilddefaulttoolsversion"" xmlns=""msbuildnamespace"">
<ItemGroup>
<a Include=""i1;i2"" />
<i Include=""@(a)"" >
{0}
</i>
</ItemGroup>
</Project>",
2, // operate on the third item. It is produced by the second item element
null
};
}
}
public static IEnumerable<object[]> ItemElementsWithGlobsThatRequireSplitting
{
get
{
// explode on item coming from glob that expands to one item
yield return new object[]
{
@"<Project ToolsVersion=""msbuilddefaulttoolsversion"" xmlns=""msbuildnamespace"">
<ItemGroup>
<a Include=""*.foo"">
{0}
</a>
</ItemGroup>
</Project>",
0,
new SetupProject(p => { p.AddItem("a", "1.foo"); })
};
// explode on item coming from glob that expands to multiple items
yield return new object[]
{
@"<Project ToolsVersion=""msbuilddefaulttoolsversion"" xmlns=""msbuildnamespace"">
<ItemGroup>
<a Include=""*.foo"">
{0}
</a>
</ItemGroup>
</Project>",
0,
new SetupProject(p =>
{
p.AddItem("a", "1.foo");
p.AddItem("a", "2.foo");
})
};
}
}
/// <summary>
/// Add an item to an empty project
/// </summary>
[Fact]
public void AddItem()
{
Project project = new Project();
project.AddItem("i", "i1");
string expected = ObjectModelHelpers.CleanupFileContents(
@"<Project ToolsVersion=""msbuilddefaulttoolsversion"" xmlns=""msbuildnamespace"">
<ItemGroup>
<i Include=""i1"" />
</ItemGroup>
</Project>");
Helpers.VerifyAssertProjectContent(expected, project.Xml);
List<ProjectItem> items = Helpers.MakeList(project.Items);
Assert.Single(items);
Assert.Equal("i", items[0].ItemType);
Assert.Equal("i1", items[0].EvaluatedInclude);
Assert.Equal("i1", Helpers.GetFirst(project.GetItems("i")).EvaluatedInclude);
Assert.Equal("i1", Helpers.MakeList(project.CreateProjectInstance().GetItems("i"))[0].EvaluatedInclude);
}
/// <summary>
/// Add an item to an empty project, where the include is escaped
/// </summary>
[Fact]
public void AddItem_EscapedItemInclude()
{
Project project = new Project();
project.AddItem("i", "i%281%29");
string expected = ObjectModelHelpers.CleanupFileContents(
@"<Project ToolsVersion=""msbuilddefaulttoolsversion"" xmlns=""msbuildnamespace"">
<ItemGroup>
<i Include=""i%281%29"" />
</ItemGroup>
</Project>");
Helpers.VerifyAssertProjectContent(expected, project.Xml);
List<ProjectItem> items = Helpers.MakeList(project.Items);
Assert.Single(items);
Assert.Equal("i", items[0].ItemType);
Assert.Equal("i(1)", items[0].EvaluatedInclude);
Assert.Equal("i(1)", Helpers.GetFirst(project.GetItems("i")).EvaluatedInclude);
Assert.Equal("i(1)", Helpers.MakeList(project.CreateProjectInstance().GetItems("i"))[0].EvaluatedInclude);
}
/// <summary>
/// Add an item with metadata
/// </summary>
[Fact]
public void AddItem_WithMetadata()
{
Project project = new Project();
List<KeyValuePair<string, string>> metadata = new List<KeyValuePair<string, string>>();
metadata.Add(new KeyValuePair<string, string>("m", "m1"));
project.AddItem("i", "i1", metadata);
string expected = ObjectModelHelpers.CleanupFileContents(
@"<Project ToolsVersion=""msbuilddefaulttoolsversion"" xmlns=""msbuildnamespace"">
<ItemGroup>
<i Include=""i1"">
<m>m1</m>
</i>
</ItemGroup>
</Project>");
Helpers.VerifyAssertProjectContent(expected, project.Xml);
}
/// <summary>
/// Add an item with empty include.
/// Should throw.
/// </summary>
[Fact]
public void AddItem_InvalidEmptyInclude()
{
Assert.Throws<ArgumentException>(() =>
{
Project project = new Project();
project.AddItem("i", String.Empty);
});
}
/// <summary>
/// Add an item with null metadata parameter.
/// Should just add no metadata.
/// </summary>
[Fact]
public void AddItem_NullMetadata()
{
Project project = new Project();
project.AddItem("i", "i1", null);
string expected = ObjectModelHelpers.CleanupFileContents(
@"<Project ToolsVersion=""msbuilddefaulttoolsversion"" xmlns=""msbuildnamespace"">
<ItemGroup>
<i Include=""i1"" />
</ItemGroup>
</Project>");
Helpers.VerifyAssertProjectContent(expected, project.Xml);
}
/// <summary>
/// Add an item whose include has a property expression. As a convenience, we attempt to expand the
/// expression to create the evaluated include.
/// </summary>
[Fact]
public void AddItem_IncludeContainsPropertyExpression()
{
Project project = new Project();
project.SetProperty("p", "v1");
project.ReevaluateIfNecessary();
project.AddItem("i", "$(p)");
Assert.Equal("$(p)", Helpers.GetFirst(project.Items).UnevaluatedInclude);
Assert.Equal("v1", Helpers.GetFirst(project.Items).EvaluatedInclude);
}
/// <summary>
/// Add an item whose include has a wildcard. We attempt to expand the wildcard using the
/// file system. In this case, we have one entry in the project and two evaluated items.
/// </summary>
[Fact]
public void AddItem_IncludeContainsWildcard()
{
string[] paths = null;
try
{
paths = Helpers.CreateFiles("i1.xxx", "i2.xxx");
string wildcard = Path.Combine(Path.GetDirectoryName(paths[0]), "*.xxx;");
Project project = new Project();
project.AddItem("i", wildcard);
List<ProjectItem> items = Helpers.MakeList(project.Items);
Assert.Equal(2, items.Count);
Assert.Equal(paths[0], items[0].EvaluatedInclude);
Assert.Equal(paths[1], items[1].EvaluatedInclude);
}
finally
{
Helpers.DeleteFiles(paths);
}
}
/// <summary>
/// Add an item whose include has an item expression. As a convenience, we attempt to expand the
/// expression to create the evaluated include.
/// This value will not be reliable until the project is reevaluated --
/// for example, it assumes any items referenced are defined above this one.
/// </summary>
[Fact]
public void AddItem_IncludeContainsItemExpression()
{
Project project = new Project();
project.AddItem("h", "h1");
project.ReevaluateIfNecessary();
ProjectItem item = project.AddItem("i", "@(h)")[0];
Assert.Equal("@(h)", item.UnevaluatedInclude);
Assert.Equal("h1", item.EvaluatedInclude);
}
/// <summary>
/// Add an item whose include contains a wildcard but doesn't match anything.
/// </summary>
[Fact]
public void AddItem_ContainingWildcardNoMatches()
{
Project project = new Project();
IList<ProjectItem> items = project.AddItem(
"i",
NativeMethodsShared.IsWindows
? @"c:\" + Guid.NewGuid().ToString() + @"\**\i1"
: "/" + Guid.NewGuid().ToString() + "/**/i1");
Assert.Empty(items);
}
/// <summary>
/// Add an item whose include contains a wildcard.
/// In this case we don't try to reuse an existing wildcard expression.
/// </summary>
[Fact]
public void AddItem_ContainingWildcardExistingWildcard()
{
Project project = new Project();
project.Xml.AddItem("i", "*.xxx");
project.ReevaluateIfNecessary();
project.AddItem("i", "*.xxx");
string expected = ObjectModelHelpers.CleanupFileContents(
@"<Project ToolsVersion=""msbuilddefaulttoolsversion"" xmlns=""msbuildnamespace"">
<ItemGroup>
<i Include=""*.xxx"" />
<i Include=""*.xxx"" />
</ItemGroup>
</Project>");
Helpers.VerifyAssertProjectContent(expected, project);
}
/// <summary>
/// Add an item whose include contains a semicolon.
/// In this case we don't try to reuse an existing wildcard expression.
/// </summary>
[Fact]
public void AddItem_ContainingSemicolonExistingWildcard()
{
Project project = new Project();
project.Xml.AddItem("i", "*.xxx");
project.ReevaluateIfNecessary();
project.AddItem("i", "i1.xxx;i2.xxx");
string expected = ObjectModelHelpers.CleanupFileContents(
@"<Project ToolsVersion=""msbuilddefaulttoolsversion"" xmlns=""msbuildnamespace"">
<ItemGroup>
<i Include=""*.xxx"" />
<i Include=""i1.xxx;i2.xxx"" />
</ItemGroup>
</Project>");
Helpers.VerifyAssertProjectContent(expected, project);
}
/// <summary>
/// If user tries to add a new item that has the same item name as an existing
/// wildcarded item, but the wildcard won't pick up the new file, then we
/// of course have to add the new item.
/// </summary>
[Fact]
public void AddItem_DoesntMatchWildcard()
{
Project project = new Project();
project.Xml.AddItem("i", "*.xxx");
project.ReevaluateIfNecessary();
project.AddItem("i", "i1");
string expected = ObjectModelHelpers.CleanupFileContents(
@"<Project ToolsVersion=""msbuilddefaulttoolsversion"" xmlns=""msbuildnamespace"">
<ItemGroup>
<i Include=""*.xxx"" />
<i Include=""i1"" />
</ItemGroup>
</Project>");
Helpers.VerifyAssertProjectContent(expected, project);
}
/// <summary>
/// When the wildcarded item already in the project file has a Condition
/// on it, we don't try to match with it when a user tries to add a new
/// item to the project.
/// </summary>
[Fact]
public void AddItem_MatchesWildcardWithCondition()
{
Project project = new Project();
ProjectItemElement itemElement = project.Xml.AddItem("i", "*.xxx");
itemElement.Condition = "true";
project.ReevaluateIfNecessary();
project.AddItem("i", "i1.xxx");
string expected = ObjectModelHelpers.CleanupFileContents(
@"<Project ToolsVersion=""msbuilddefaulttoolsversion"" xmlns=""msbuildnamespace"">
<ItemGroup>
<i Include=""*.xxx"" Condition=""true"" />
<i Include=""i1.xxx"" />
</ItemGroup>
</Project>");
Helpers.VerifyAssertProjectContent(expected, project);
}
/// <summary>
/// When the wildcarded item already in the project file has a Exclude
/// on it, we don't try to match with it when a user tries to add a new
/// item to the project.
/// </summary>
[Fact]
public void AddItem_MatchesWildcardWithExclude()
{
Project project = new Project();
ProjectItemElement itemElement = project.Xml.AddItem("i", "*.xxx");
itemElement.Exclude = "i2.xxx";
project.ReevaluateIfNecessary();
project.AddItem("i", "i1.xxx");
string expected = ObjectModelHelpers.CleanupFileContents(
@"<Project ToolsVersion=""msbuilddefaulttoolsversion"" xmlns=""msbuildnamespace"">
<ItemGroup>
<i Include=""*.xxx"" Exclude=""i2.xxx"" />
<i Include=""i1.xxx"" />
</ItemGroup>
</Project>");
Helpers.VerifyAssertProjectContent(expected, project);
}
/// <summary>
/// There's a wildcard in the project already, and the user tries to add an item
/// that matches that wildcard. In this case, we don't touch the project at all.
/// </summary>
[Fact]
public void AddItem_MatchesWildcard()
{
Project project = new Project();
ProjectItemElement item1 = project.Xml.AddItem("i", "*.xxx");
project.ReevaluateIfNecessary();
ProjectItemElement item2 = project.AddItem("i", "i1.xxx")[0].Xml;
string expected = ObjectModelHelpers.CleanupFileContents(
@"<Project ToolsVersion=""msbuilddefaulttoolsversion"" xmlns=""msbuildnamespace"">
<ItemGroup>
<i Include=""*.xxx"" />
</ItemGroup>
</Project>");
Helpers.VerifyAssertProjectContent(expected, project);
Assert.True(object.ReferenceEquals(item1, item2));
}
/// <summary>
/// There's a wildcard in the project already, and the user tries to add an item
/// that matches that wildcard, except that its item type is different.
/// In this case, we ignore the existing wildcard.
/// </summary>
[Fact]
public void AddItem_MatchesWildcardButNotItemType()
{
Project project = new Project();
project.Xml.AddItem("i", "*.xxx");
project.ReevaluateIfNecessary();
project.AddItem("j", "j1.xxx");
string expected = ObjectModelHelpers.CleanupFileContents(
@"<Project ToolsVersion=""msbuilddefaulttoolsversion"" xmlns=""msbuildnamespace"">
<ItemGroup>
<i Include=""*.xxx"" />
</ItemGroup>
<ItemGroup>
<j Include=""j1.xxx"" />
</ItemGroup>
</Project>");
Helpers.VerifyAssertProjectContent(expected, project);
}
/// <summary>
/// There's a complicated recursive wildcard in the project already, and the user tries to add an item
/// that matches that wildcard. In this case, we don't touch the project at all.
/// </summary>
[Fact]
public void AddItem_MatchesComplicatedWildcard()
{
Project project = new Project();
ProjectItemElement item1 = project.Xml.AddItem(
"i",
NativeMethodsShared.IsWindows ? @"c:\subdir1\**\subdir2\**\*.x?x" : "/subdir1/**/subdir2/**/*.x?x");
project.ReevaluateIfNecessary();
ProjectItemElement item2 =
project.AddItem(
"i",
NativeMethodsShared.IsWindows ? @"c:\subdir1\a\b\subdir2\c\i1.xyx" : "/subdir1/a/b/subdir2/c/i1.xyx")[
0].Xml;
string expected = ObjectModelHelpers.CleanupFileContents(
NativeMethodsShared.IsWindows ?
@"<Project ToolsVersion=""msbuilddefaulttoolsversion"" xmlns=""msbuildnamespace"">
<ItemGroup>
<i Include=""c:\subdir1\**\subdir2\**\*.x?x"" />
</ItemGroup>
</Project>" :
@"<Project ToolsVersion=""msbuilddefaulttoolsversion"" xmlns=""msbuildnamespace"">
<ItemGroup>
<i Include=""/subdir1/**/subdir2/**/*.x?x"" />
</ItemGroup>
</Project>");
Helpers.VerifyAssertProjectContent(expected, project);
Assert.True(object.ReferenceEquals(item1, item2));
}
/// <summary>
/// There's a complicated recursive wildcard in the project already, and the user tries to add an item
/// that doesn't match that wildcard. In this case, we don't touch the project at all.
/// </summary>
[Fact]
public void AddItem_DoesntMatchComplicatedWildcard()
{
Project project = new Project();
ProjectItemElement item1 = project.Xml.AddItem("i", @"c:\subdir1\**\subdir2\**\*.x?x");
project.ReevaluateIfNecessary();
ProjectItemElement item2 = project.AddItem("i", @"c:\subdir1\a\b\c\i1.xyx")[0].Xml;
string expected = ObjectModelHelpers.CleanupFileContents(
@"<Project ToolsVersion=""msbuilddefaulttoolsversion"" xmlns=""msbuildnamespace"">
<ItemGroup>
<i Include=""c:\subdir1\**\subdir2\**\*.x?x"" />
<i Include=""c:\subdir1\a\b\c\i1.xyx"" />
</ItemGroup>
</Project>");
Helpers.VerifyAssertProjectContent(expected, project);
Assert.False(object.ReferenceEquals(item1, item2));
}
/// <summary>
/// There's a wildcard in the project already, and the user tries to add an item
/// that matches that wildcard. In this case, we add a new item, because the old
/// one wasn't equivalent.
/// In contrast Orcas/Whidbey assumed that the user wants
/// that metadata on the new item, too.
/// </summary>
[Fact]
public void AddItem_DoesNotMatchWildcardWithMetadata()
{
Project project = new Project();
ProjectItemElement item1 = project.Xml.AddItem("i", "*.xxx");
item1.AddMetadata("m", "m1");
project.ReevaluateIfNecessary();
project.AddItem("i", "i1.xxx");
string expected = ObjectModelHelpers.CleanupFileContents(
@"<Project ToolsVersion=""msbuilddefaulttoolsversion"" xmlns=""msbuildnamespace"">
<ItemGroup>
<i Include=""*.xxx"">
<m>m1</m>
</i>
<i Include=""i1.xxx"" />
</ItemGroup>
</Project>");
Helpers.VerifyAssertProjectContent(expected, project);
}
/// <summary>
/// There's a wildcard in the project already, and the user tries to add an item
/// with metadata. In this case, we add a new item, because the old
/// one wasn't equivalent.
/// </summary>
[Fact]
public void AddItemWithMetadata_DoesNotMatchWildcardWithNoMetadata()
{
Project project = new Project();
project.Xml.AddItem("i", "*.xxx");
project.ReevaluateIfNecessary();
Dictionary<string, string> metadata = new Dictionary<string, string>() { { "m", "m1" } };
project.AddItem("i", "i1.xxx", metadata);
string expected = ObjectModelHelpers.CleanupFileContents(
@"<Project ToolsVersion=""msbuilddefaulttoolsversion"" xmlns=""msbuildnamespace"">
<ItemGroup>
<i Include=""*.xxx"" />
<i Include=""i1.xxx"">
<m>m1</m>
</i>
</ItemGroup>
</Project>");
Helpers.VerifyAssertProjectContent(expected, project);
}
/// <summary>
/// There's a wildcard in the project already, but it's part of a semicolon-separated
/// list of items. Now the user tries to add an item that matches that wildcard.
/// In this case, we don't touch the project at all.
/// </summary>
[Fact]
public void AddItem_MatchesWildcardInSemicolonList()
{
Project project = new Project();
project.Xml.AddItem("i", "a;*.xxx;b");
project.ReevaluateIfNecessary();
project.AddItem("i", "i1.xxx");
string expected = ObjectModelHelpers.CleanupFileContents(
@"<Project ToolsVersion=""msbuilddefaulttoolsversion"" xmlns=""msbuildnamespace"">
<ItemGroup>
<i Include=""a;*.xxx;b"" />
</ItemGroup>
</Project>");
Helpers.VerifyAssertProjectContent(expected, project);
}
/// <summary>
/// Modify an item originating in a wildcard by adding a new piece of metadata.
/// We should blow up the item in the project file.
/// </summary>
[Fact]
public void SetMetadata_ItemOriginatingWithWildcard()
{
string[] paths = null;
try
{
paths = Helpers.CreateFiles("i1.xxx", "i2.xxx");
string directory = Path.GetDirectoryName(paths[0]);
string wildcard = Path.Combine(directory, "*.xxx;");
Project project = new Project();
ProjectItemElement itemElement = project.Xml.AddItem("i", wildcard);
itemElement.AddMetadata("m", "m0");
project.ReevaluateIfNecessary();
Helpers.GetFirst(project.GetItems("i")).SetMetadataValue("n", "n1");
string expected = string.Format(
ObjectModelHelpers.CleanupFileContents(
@"<Project ToolsVersion=""msbuilddefaulttoolsversion"" xmlns=""msbuildnamespace"">
<ItemGroup>
<i Include=""{0}"">
<m>m0</m>
<n>n1</n>
</i>
<i Include=""{1}"">
<m>m0</m>
</i>
</ItemGroup>
</Project>"),
ProjectCollection.Escape(paths[0]),
ProjectCollection.Escape(paths[1]));
Helpers.VerifyAssertProjectContent(expected, project.Xml);
}
finally
{
Helpers.DeleteFiles(paths);
}
}
/// <summary>
/// Set a piece of metadata on an item originating from an item list expression.
/// We should blow up the expression and set the metadata on one of the resulting items.
/// </summary>
[Fact]
public void SetMetadata_ItemOriginatingWithItemList()
{
var content = ObjectModelHelpers.CleanupFileContents(
@"<Project ToolsVersion=""msbuilddefaulttoolsversion"" xmlns=""msbuildnamespace"">
<ItemGroup>
<h Include=""h1;h2"">
<m>m1</m>
</h>
<i Include=""@(h)"" />
</ItemGroup>
</Project>");
using ProjectFromString projectFromString = new(content);
Project project = projectFromString.Project;
Helpers.GetFirst(project.GetItems("i")).SetMetadataValue("m", "m2");
string expected = ObjectModelHelpers.CleanupFileContents(
@"<Project ToolsVersion=""msbuilddefaulttoolsversion"" xmlns=""msbuildnamespace"">
<ItemGroup>
<h Include=""h1;h2"">
<m>m1</m>
</h>
<i Include=""h1"">
<m>m2</m>
</i>
<i Include=""h2"">
<m>m1</m>
</i>
</ItemGroup>
</Project>");
Helpers.VerifyAssertProjectContent(expected, project.Xml, false);
}
/// <summary>
/// Change the value on a piece of metadata on an item originating from an item list expression.
/// The ProjectMetadata object is shared by all the items here, so the edit does not cause any expansion.
/// </summary>
[Fact]
public void SetMetadataUnevaluatedValue_ItemOriginatingWithItemList()
{
var content = ObjectModelHelpers.CleanupFileContents(
@"<Project ToolsVersion=""msbuilddefaulttoolsversion"" xmlns=""msbuildnamespace"">
<ItemGroup>
<h Include=""h1;h2"">
<m>m1</m>
</h>
<i Include=""@(h)"" />
</ItemGroup>
</Project>");
using ProjectFromString projectFromString = new(content);
Project project = projectFromString.Project;
ProjectMetadata metadatum = Helpers.GetFirst(project.GetItems("i")).GetMetadata("m");
metadatum.UnevaluatedValue = "m2";
string expected = ObjectModelHelpers.CleanupFileContents(
@"<Project ToolsVersion=""msbuilddefaulttoolsversion"" xmlns=""msbuildnamespace"">
<ItemGroup>
<h Include=""h1;h2"">
<m>m2</m>
</h>
<i Include=""@(h)"" />
</ItemGroup>
</Project>");
Helpers.VerifyAssertProjectContent(expected, project.Xml, false);
}
/// <summary>
/// Modify an item originating in a wildcard by removing a piece of metadata.
/// We should blow up the item in the project file.
/// </summary>
[Fact]
public void RemoveMetadata_ItemOriginatingWithWildcard()
{
string[] paths = null;
try
{
paths = Helpers.CreateFiles("i1.xxx", "i2.xxx");
string directory = Path.GetDirectoryName(paths[0]);
string wildcard = Path.Combine(directory, "*.xxx;");
Project project = new Project();
ProjectItemElement itemElement = project.Xml.AddItem("i", wildcard);
itemElement.AddMetadata("m", "m1");
project.ReevaluateIfNecessary();
Helpers.GetFirst(project.GetItems("i")).RemoveMetadata("m");
string expected = string.Format(
ObjectModelHelpers.CleanupFileContents(
@"<Project ToolsVersion=""msbuilddefaulttoolsversion"" xmlns=""msbuildnamespace"">
<ItemGroup>
<i Include=""{0}"" />
<i Include=""{1}"">
<m>m1</m>
</i>
</ItemGroup>
</Project>"),
ProjectCollection.Escape(paths[0]),
ProjectCollection.Escape(paths[1]));
Helpers.VerifyAssertProjectContent(expected, project.Xml);
}
finally
{
Helpers.DeleteFiles(paths);
}
}
/// <summary>
/// There's a wildcard in the project already, but it's part of a semicolon-separated
/// list of items, and it uses a property reference. Now the user tries to add a new
/// item that matches that wildcard. In this case, we don't touch the project at all.
/// </summary>
[Fact]
public void AddItem_MatchesWildcardWithPropertyReference()
{
Project project = new Project();
project.SetProperty("p", "xxx");
project.Xml.AddItem("i", "a;*.$(p);b");
project.ReevaluateIfNecessary();
project.AddItem("i", "i1.xxx");
string expected = ObjectModelHelpers.CleanupFileContents(
@"<Project ToolsVersion=""msbuilddefaulttoolsversion"" xmlns=""msbuildnamespace"">
<PropertyGroup>
<p>xxx</p>
</PropertyGroup>
<ItemGroup>
<i Include=""a;*.$(p);b"" />
</ItemGroup>
</Project>");
Helpers.VerifyAssertProjectContent(expected, project);
}
/// <summary>
/// There's a wildcard in the project already, and the user renames an item such that it
/// now matches that wildcard. We don't try to do any thing clever like reuse that wildcard.
/// </summary>
[Fact]
public void RenameItem_MatchesWildcard()
{
Project project = new Project();
project.AddItem("i", "*.xxx");
project.AddItem("i", "i1");
project.ReevaluateIfNecessary();
ProjectItem item = Helpers.GetLast(project.Items);
item.Rename("i1.xxx");
string expected = ObjectModelHelpers.CleanupFileContents(
@"<Project ToolsVersion=""msbuilddefaulttoolsversion"" xmlns=""msbuildnamespace"">
<ItemGroup>
<i Include=""*.xxx"" />
<i Include=""i1.xxx"" />
</ItemGroup>
</Project>");
Helpers.VerifyAssertProjectContent(expected, project);
}
/// <summary>
/// Rename, with the new name containing a property expression.
/// Because the rename did not cause more items to appear, it is possible
/// to update the EvaluatedInclude of this one.
/// </summary>
[Fact]
public void RenameItem_NewNameContainsPropertyExpression()
{
Project project = new Project();
project.SetProperty("p", "v1");
project.AddItem("i", "i1");
project.ReevaluateIfNecessary();
ProjectItem item = Helpers.GetFirst(project.Items);
item.Rename("$(p)");
Assert.Equal("$(p)", item.UnevaluatedInclude);
// Rename should have been expanded in this simple case
Assert.Equal("v1", item.EvaluatedInclude);
// The ProjectItemElement should be the same
ProjectItemElement newItemElement = Helpers.GetFirst((Helpers.GetFirst(project.Xml.ItemGroups)).Items);
Assert.True(object.ReferenceEquals(item.Xml, newItemElement));
}
/// <summary>
/// Rename, with the new name containing an item expression.
/// Because the rename did not cause more items to appear, it is possible
/// to update the EvaluatedInclude of this one.
/// </summary>
[Fact]
public void RenameItem_NewNameContainsItemExpression()
{
Project project = new Project();
project.SetProperty("p", "v1");
project.AddItem("h", "h1");
project.AddItem("i", "i1");
project.ReevaluateIfNecessary();
ProjectItem item = Helpers.GetLast(project.Items);
item.Rename("@(h)");
Assert.Equal("@(h)", item.UnevaluatedInclude);
// Rename should have been expanded in this simple case
Assert.Equal("h1", item.EvaluatedInclude);
// The ProjectItemElement should be the same
ProjectItemElement newItemElement = Helpers.GetLast((Helpers.GetLast(project.Xml.ItemGroups)).Items);
Assert.True(object.ReferenceEquals(item.Xml, newItemElement));
}
/// <summary>
/// Rename, with the new name containing an item expression.
/// Because the new name expands to more than one item, we don't attempt to
/// update the evaluated include.
/// </summary>
[Fact]
public void RenameItem_NewNameContainsItemExpressionExpandingToTwoItems()
{
Project project = new Project();
project.AddItem("h", "h1");
project.AddItem("h", "h2");
project.AddItem("i", "i1");
project.ReevaluateIfNecessary();
ProjectItem item = Helpers.GetLast(project.Items);
item.Rename("@(h)");
Assert.Equal("@(h)", item.UnevaluatedInclude);
Assert.Equal("@(h)", item.EvaluatedInclude);
// The ProjectItemElement should be the same
ProjectItemElement newItemElement = Helpers.GetLast((Helpers.GetLast(project.Xml.ItemGroups)).Items);
Assert.True(object.ReferenceEquals(item.Xml, newItemElement));
}
/// <summary>
/// Rename, with the new name containing an item expression.
/// Because the new name expands to not exactly one item, we don't attempt to
/// update the evaluated include.
/// Reasoning: The case we interested in for expansion here is setting something
/// like "$(sourcesroot)\foo.cs" and expanding that to a single item.
/// If say "@(foo)" is set as the new name, and it expands to blank, that might
/// be surprising to the host and maybe even unhandled, if on full reevaluation
/// it wouldn't expand to blank. That's why I'm being cautious and supporting
/// the most common scenario only. Many hosts will do a ReevaluateIfNecessary before reading anyway (including CPS)
/// </summary>
[Fact]
public void RenameItem_NewNameContainsItemExpressionExpandingToZeroItems()
{
Project project = new Project();
project.AddItem("i", "i1");
project.ReevaluateIfNecessary();
ProjectItem item = Helpers.GetLast(project.Items);
item.Rename("@(h)");
Assert.Equal("@(h)", item.UnevaluatedInclude);
Assert.Equal("@(h)", item.EvaluatedInclude);
}
/// <summary>
/// Rename an item that originated in an expression like "@(h)"
/// We should blow up the expression and rename the correct part.
/// </summary>
[Fact]
public void RenameItem_OriginatingWithItemList()
{
var content = ObjectModelHelpers.CleanupFileContents(
@"<Project ToolsVersion=""msbuilddefaulttoolsversion"" xmlns=""msbuildnamespace"">
<ItemGroup>
<h Include=""h1;h2"">
<m>m1</m>
</h>
<i Include=""@(h)"" />
</ItemGroup>
</Project>");
using ProjectFromString projectFromString = new(content);
Project project = projectFromString.Project;
ProjectItem item = Helpers.GetFirst(project.GetItems("i"));
item.Rename("h1b");
string expected = ObjectModelHelpers.CleanupFileContents(
@"<Project ToolsVersion=""msbuilddefaulttoolsversion"" xmlns=""msbuildnamespace"">
<ItemGroup>
<h Include=""h1;h2"">
<m>m1</m>
</h>
<i Include=""h1b"">
<m>m1</m>
</i>
<i Include=""h2"">
<m>m1</m>
</i>
</ItemGroup>
</Project>");
Helpers.VerifyAssertProjectContent(expected, project.Xml, false);
}
/// <summary>
/// Rename an item that originated in an expression like "a.cs;b.cs"
/// We should blow up the expression and rename the correct part.
/// </summary>
[Fact]
public void RenameItem_OriginatingWithSemicolon()
{
Project project = new Project();
project.Xml.AddItem("i", "i1;i2;i3");
project.ReevaluateIfNecessary();
ProjectItem item = Helpers.MakeList(project.GetItems("i"))[1];
item.Rename("i2b");
string expected = ObjectModelHelpers.CleanupFileContents(
@"<Project ToolsVersion=""msbuilddefaulttoolsversion"" xmlns=""msbuildnamespace"">
<ItemGroup>
<i Include=""i1"" />
<i Include=""i2b"" />
<i Include=""i3"" />
</ItemGroup>
</Project>");
Helpers.VerifyAssertProjectContent(expected, project.Xml);
}
/// <summary>
/// Rename an item that originated in an expression like "a.cs;b.cs"
/// to a property expression.
/// We should blow up the expression and rename the correct part,
/// and because a split had to occur, we should not expand the expression.
/// </summary>
[Fact]
public void RenameItem_OriginatingWithSemicolonToExpandableExpression()
{
Project project = new Project();
project.SetProperty("p", "v1");
project.Xml.AddItem("i", "i1;i2;i3");
project.ReevaluateIfNecessary();
ProjectItem item = Helpers.MakeList(project.GetItems("i"))[1];
item.Rename("$(p)");
string expected = ObjectModelHelpers.CleanupFileContents(
@"<Project ToolsVersion=""msbuilddefaulttoolsversion"" xmlns=""msbuildnamespace"">
<PropertyGroup>
<p>v1</p>
</PropertyGroup>
<ItemGroup>
<i Include=""i1"" />
<i Include=""$(p)"" />
<i Include=""i3"" />
</ItemGroup>
</Project>");
Helpers.VerifyAssertProjectContent(expected, project.Xml);
Assert.Equal("$(p)", (Helpers.MakeList(project.Items))[1].EvaluatedInclude);
}
/// <summary>
/// An item originates from a wildcard, and we rename it to something
/// that no longer matches the wildcard. This should cause the wildcard to be expanded.
/// </summary>
[Fact]
public void RenameItem_NoLongerMatchesWildcard()
{
string[] paths = null;
try
{
paths = Helpers.CreateFiles("i1.xxx", "i2.xxx");
string directory = Path.GetDirectoryName(paths[0]);
string wildcard = Path.Combine(directory, "*.xxx;");
Project project = new Project();
project.Xml.AddItem("i", wildcard);
project.ReevaluateIfNecessary();
ProjectItem item = Helpers.GetFirst(project.Items);
item.Rename("i1.yyy");
string expected = string.Format(
ObjectModelHelpers.CleanupFileContents(
@"<Project ToolsVersion=""msbuilddefaulttoolsversion"" xmlns=""msbuildnamespace"">
<ItemGroup>
<i Include=""i1.yyy"" />
<i Include=""{0}"" />
</ItemGroup>
</Project>"),
ProjectCollection.Escape(Path.Combine(directory, "i2.xxx")));
Helpers.VerifyAssertProjectContent(expected, project.Xml);
}
finally
{
Helpers.DeleteFiles(paths);
}
}
/// <summary>
/// An item originates from a wildcard, and we rename it to something
/// that still matches the wildcard. This should not modify the project.
/// </summary>
[Fact]
public void RenameItem_StillMatchesWildcard()
{
string[] paths = null;
try
{
paths = Helpers.CreateFiles("i1.xxx");
string directory = Path.GetDirectoryName(paths[0]);
string wildcard = Path.Combine(directory, "*.xxx;");
Project project = new Project();
project.AddItem("i", wildcard);
project.ReevaluateIfNecessary();
string before = project.Xml.RawXml;
ProjectItem item = Helpers.GetFirst(project.Items);
item.Rename(Path.Combine(directory, "i2.xxx"));
Helpers.VerifyAssertLineByLine(before, project.Xml.RawXml);
}
finally
{
Helpers.DeleteFiles(paths);
}
}
[Theory]
[MemberData(nameof(ItemElementsThatRequireSplitting))]
[MemberData(nameof(ItemElementsWithGlobsThatRequireSplitting))]
public void RenameThrowsWhenItemElementSplittingIsDisabled(string projectContents, int itemIndex, SetupProject setupProject)
{
AssertDisabledItemSplitting(projectContents, itemIndex, setupProject, (p, i) => { i.Rename("foo"); });
}
/// <summary>
/// Change an item type.
/// </summary>
[Fact]
public void ChangeItemType()
{
Project project = new Project();
project.AddItem("i", "i1");
project.ReevaluateIfNecessary();
ProjectItem item = Helpers.GetFirst(project.GetItems("i"));
item.ItemType = "j";
Assert.Equal("j", item.ItemType);
string expected = ObjectModelHelpers.CleanupFileContents(
@"<Project ToolsVersion=""msbuilddefaulttoolsversion"" xmlns=""msbuildnamespace"">
<ItemGroup>
<j Include=""i1"" />
</ItemGroup>
</Project>");
Helpers.VerifyAssertProjectContent(expected, project.Xml);
ProjectItemGroupElement itemGroupElement = Helpers.GetFirst(project.Xml.ItemGroups);
Assert.Single(Helpers.MakeList(itemGroupElement.Items));
Assert.True(object.ReferenceEquals(itemGroupElement, item.Xml.Parent));
Assert.Single(Helpers.MakeList(project.Items));
Assert.Single(Helpers.MakeList(project.ItemsIgnoringCondition));
Assert.Empty(Helpers.MakeList(project.GetItems("i")));
Assert.Empty(Helpers.MakeList(project.GetItemsIgnoringCondition("i")));
Assert.True(object.ReferenceEquals(item, Helpers.GetFirst(project.GetItems("j"))));
Assert.True(object.ReferenceEquals(item, Helpers.GetFirst(project.GetItemsIgnoringCondition("j"))));
Assert.True(object.ReferenceEquals(item, Helpers.GetFirst(project.GetItemsByEvaluatedInclude("i1"))));
}
/// <summary>
/// Change an item type; metadata should stay in place
/// </summary>
[Fact]
public void ChangeItemTypeOnItemWithMetadata()
{
Project project = new Project();
ProjectItem item0 = project.AddItem("i", "i1")[0];
item0.Xml.Exclude = "e";
ProjectMetadataElement metadatumElement1 = item0.SetMetadataValue("m", "m1").Xml;
metadatumElement1.Condition = "true";
item0.SetMetadataValue("n", "n1");
project.ReevaluateIfNecessary();
ProjectItem item = Helpers.GetFirst(project.GetItems("i"));
item.ItemType = "j";
Assert.Equal("j", item.ItemType);
string expected = ObjectModelHelpers.CleanupFileContents(
@"<Project ToolsVersion=""msbuilddefaulttoolsversion"" xmlns=""msbuildnamespace"">
<ItemGroup>
<j Include=""i1"" Exclude=""e"">
<m Condition=""true"">m1</m>
<n>n1</n>
</j>
</ItemGroup>
</Project>");
Helpers.VerifyAssertProjectContent(expected, project.Xml);
// Item element identity changed unfortunately, but metadata elements should be the same objects.
ProjectItemElement itemElement = Helpers.GetFirst(Helpers.GetFirst(project.Xml.ItemGroups).Items);
Assert.True(object.ReferenceEquals(itemElement, metadatumElement1.Parent));
Assert.Equal(2, Helpers.MakeList(itemElement.Metadata).Count);
Assert.Equal(2 + 15 /* built-in metadata */, item.MetadataCount);
Assert.Equal("n1", item.GetMetadataValue("n"));
// Remove one piece of metadata, to hopefully help verify that the DOM is in a good state
item.RemoveMetadata("m");
expected = ObjectModelHelpers.CleanupFileContents(
@"<Project ToolsVersion=""msbuilddefaulttoolsversion"" xmlns=""msbuildnamespace"">
<ItemGroup>
<j Include=""i1"" Exclude=""e"">
<n>n1</n>
</j>
</ItemGroup>
</Project>");
Helpers.VerifyAssertProjectContent(expected, project.Xml);
}
/// <summary>
/// Change an item type where the item needs blowing up first.
/// </summary>
[Fact]
public void ChangeItemTypeOnItemNeedingSplitting()
{
Project project = new Project();
project.Xml.AddItem("i", "i1;i2");
project.ReevaluateIfNecessary();
ProjectItem item = Helpers.GetFirst(project.GetItems("i"));
item.ItemType = "j";
string expected = ObjectModelHelpers.CleanupFileContents(
@"<Project ToolsVersion=""msbuilddefaulttoolsversion"" xmlns=""msbuildnamespace"">
<ItemGroup>
<j Include=""i1"" />
<i Include=""i2"" />
</ItemGroup>
</Project>");
Helpers.VerifyAssertProjectContent(expected, project.Xml);
ProjectItemGroupElement itemGroupElement = Helpers.GetFirst(project.Xml.ItemGroups);
Assert.Equal(2, Helpers.MakeList(itemGroupElement.Items).Count);
Assert.True(object.ReferenceEquals(itemGroupElement, item.Xml.Parent));
Assert.True(object.ReferenceEquals(itemGroupElement, Helpers.GetFirst(project.GetItems("i")).Xml.Parent));
}
[Theory]
[MemberData(nameof(ItemElementsThatRequireSplitting))]
[MemberData(nameof(ItemElementsWithGlobsThatRequireSplitting))]
public void ChangeItemTypeThrowsWhenItemElementSplittingIsDisabled(string projectContents, int itemIndex, SetupProject setupProject)
{
AssertDisabledItemSplitting(projectContents, itemIndex, setupProject, (p, i) => { i.ItemType = "foo"; });
}
/// <summary>
/// Remove an item, clearing up the empty item group as well
/// </summary>
[Fact]
public void RemoveItem()
{
Project project = new Project();
project.AddItem("i", "i1");
project.ReevaluateIfNecessary();
project.RemoveItem(Helpers.GetFirst(project.GetItems("i")));
string expected = ObjectModelHelpers.CleanupFileContents(@"<Project ToolsVersion=""msbuilddefaulttoolsversion"" xmlns=""msbuildnamespace"" />");
Helpers.VerifyAssertProjectContent(expected, project.Xml);
Assert.Equal(0, Helpers.Count(project.Items));
Assert.Empty(Helpers.MakeList(project.CreateProjectInstance().GetItems("i")));
}
/// <summary>
/// Remove an item that originated in an expression like "@(h)"
/// We should expand the expression to the remaining items, if any.
/// Metadata should be preserved.
/// </summary>
[Fact]
public void RemoveItem_OriginatingWithItemList()
{
var content = ObjectModelHelpers.CleanupFileContents(
@"<Project ToolsVersion=""msbuilddefaulttoolsversion"" xmlns=""msbuildnamespace"">
<ItemGroup>
<h Include=""h1;h2"">
<m>m1</m>
</h>
<i Include=""@(h)"" />
</ItemGroup>
</Project>");
using ProjectFromString projectFromString = new(content);
Project project = projectFromString.Project;
project.RemoveItem(Helpers.GetFirst(project.GetItems("i")));
string expected = ObjectModelHelpers.CleanupFileContents(
@"<Project ToolsVersion=""msbuilddefaulttoolsversion"" xmlns=""msbuildnamespace"">
<ItemGroup>
<h Include=""h1;h2"">
<m>m1</m>
</h>
<i Include=""h2"">
<m>m1</m>
</i>
</ItemGroup>
</Project>");
Helpers.VerifyAssertProjectContent(expected, project.Xml, false);
}
/// <summary>
/// Remove an item that originated in an expression like "a.cs;b.cs"
/// We should keep the part of the expression that still applies.
/// </summary>
[Fact]
public void RemoveItem_OriginatingWithSemicolon()
{
Project project = new Project();
project.Xml.AddItem("i", "i1;i2");
project.ReevaluateIfNecessary();
project.RemoveItem(Helpers.GetFirst(project.GetItems("i")));
string expected = ObjectModelHelpers.CleanupFileContents(
@"<Project ToolsVersion=""msbuilddefaulttoolsversion"" xmlns=""msbuildnamespace"">
<ItemGroup>
<i Include=""i2"" />
</ItemGroup>
</Project>");
Helpers.VerifyAssertProjectContent(expected, project.Xml);
}
/// <summary>
/// Remove an item originating from a wildcard
/// This should cause the wildcard to be expanded to the remaining items, if any.
/// Expanding the wildcard should preserve the metadata on it.
/// </summary>
[Fact]
public void RemoveItem_OriginatingWithWildcard()
{
string[] paths = null;
try
{
paths = Helpers.CreateFiles("i1.xxx", "i2.xxx");
string directory = Path.GetDirectoryName(paths[0]);
string wildcard = Path.Combine(directory, "*.xxx;");
Project project = new Project();
ProjectItemElement itemElement = project.Xml.AddItem("i", wildcard);
itemElement.AddMetadata("m", "m1");
project.ReevaluateIfNecessary();
ProjectItem item = Helpers.GetFirst(project.Items);
project.RemoveItem(item);
string expected = string.Format(
ObjectModelHelpers.CleanupFileContents(
@"<Project ToolsVersion=""msbuilddefaulttoolsversion"" xmlns=""msbuildnamespace"">
<ItemGroup>
<i Include=""{0}"">
<m>m1</m>
</i>
</ItemGroup>
</Project>"),
ProjectCollection.Escape(Path.Combine(directory, "i2.xxx")));
Helpers.VerifyAssertProjectContent(expected, project.Xml);
}
finally
{
Helpers.DeleteFiles(paths);
}
}
/// <summary>
/// Items in certain locations are stored by the project despite having a false condition -- eg for populating the solution explorer.
/// Removing an item should remove it from this list too.
/// </summary>
[Fact]
public void RemoveItem_IncludingFromIgnoringConditionList()
{
var content = ObjectModelHelpers.CleanupFileContents(
@"<Project ToolsVersion=""msbuilddefaulttoolsversion"" xmlns=""msbuildnamespace"">
<ItemGroup Condition=""false"">
<i Include=""i1"" />
</ItemGroup>
</Project>");
using ProjectFromString projectFromString = new(content);
Project project = projectFromString.Project;
Assert.Empty(Helpers.MakeList(project.GetItems("i")));
List<ProjectItem> itemsIgnoringCondition = Helpers.MakeList(project.GetItemsIgnoringCondition("i"));
Assert.Single(itemsIgnoringCondition);
ProjectItem item = itemsIgnoringCondition[0];
Assert.Equal("i1", item.EvaluatedInclude);
bool result = project.RemoveItem(item);
Assert.False(result); // false as it was not in the regular items collection
itemsIgnoringCondition = Helpers.MakeList(project.GetItemsIgnoringCondition("i"));
Assert.Empty(itemsIgnoringCondition);
}
[Theory]
[MemberData(nameof(ItemElementsThatRequireSplitting))]
[MemberData(nameof(ItemElementsWithGlobsThatRequireSplitting))]
public void RemoveItemThrowsWhenItemElementSplittingIsDisabled(string projectContents, int itemIndex, SetupProject setupProject)
{
AssertDisabledItemSplitting(projectContents, itemIndex, setupProject, (p, i) => { p.RemoveItem(i); });
}
[Theory]
[MemberData(nameof(ItemElementsThatRequireSplitting))]
[MemberData(nameof(ItemElementsWithGlobsThatRequireSplitting))]
public void RemoveItemsThrowsWhenItemElementSplittingIsDisabled(string projectContents, int itemIndex, SetupProject setupProject)
{
AssertDisabledItemSplitting(projectContents, itemIndex, setupProject, (p, i) => { p.RemoveItems(new[] { i }); });
}
/// <summary>
/// Test simple property set with name and value
/// </summary>
[Fact]
public void SetProperty()
{
Project project = new Project();
int environmentPropertyCount = Helpers.MakeList(project.Properties).Count;
project.SetProperty("p1", "v1");
string expected = ObjectModelHelpers.CleanupFileContents(
@"<Project ToolsVersion=""msbuilddefaulttoolsversion"" xmlns=""msbuildnamespace"">
<PropertyGroup>
<p1>v1</p1>
</PropertyGroup>
</Project>");
Helpers.VerifyAssertProjectContent(expected, project.Xml);
Assert.Equal("v1", project.GetPropertyValue("p1"));
Assert.Equal("v1", project.CreateProjectInstance().GetPropertyValue("p1"));
Assert.Equal(1, Helpers.Count(project.Properties) - environmentPropertyCount);
}
/// <summary>
/// Test simple property set with name and value, where the value is escaped
/// </summary>
[Fact]
public void SetProperty_EscapedValue()
{
Project project = new Project();
int environmentPropertyCount = Helpers.MakeList(project.Properties).Count;
project.SetProperty("p1", "v%5E1");
string expected = ObjectModelHelpers.CleanupFileContents(
@"<Project ToolsVersion=""msbuilddefaulttoolsversion"" xmlns=""msbuildnamespace"">
<PropertyGroup>
<p1>v%5E1</p1>
</PropertyGroup>
</Project>");
Helpers.VerifyAssertProjectContent(expected, project.Xml);
Assert.Equal("v^1", project.GetPropertyValue("p1"));
Assert.Equal("v^1", project.CreateProjectInstance().GetPropertyValue("p1"));
Assert.Equal(1, Helpers.Count(project.Properties) - environmentPropertyCount);
}
/// <summary>
/// Setting a property that originates in an import should not try to edit the property there.
/// It should set it in the main project file.
/// </summary>
[Fact]
public void SetPropertyOriginatingInImport()
{
ProjectRootElement xml = ProjectRootElement.Create();
xml.AddImport(NativeMethodsShared.IsWindows ?
"$(msbuildtoolspath)\\microsoft.common.targets" :
"$(msbuildtoolspath)/Microsoft.Common.targets");
Project project = new Project(xml);
// This property certainly exists in that imported file
project.SetProperty("OutDir", "foo"); // should not throw
Assert.Equal("foo", project.GetPropertyValue("OutDir"));
Assert.Single(Helpers.MakeList(xml.Properties));
}
/// <summary>
/// Verify properties are expanded in new property values
/// </summary>
[Fact]
public void SetPropertyWithPropertyExpression()
{
Project project = new Project();
project.SetProperty("p0", "v0");
project.SetProperty("p1", "$(p0)");
Assert.Equal("v0", project.GetPropertyValue("p1"));
}
/// <summary>
/// Verify item expressions are not expanded in new property values.
/// NOTE: They aren't expanded to "blank". It just seems like that, because
/// when you output them, item expansion happens after property expansion, and
/// they may evaluate to blank then. (Unless items do exist at that point.)
/// </summary>
[Fact]
public void SetPropertyWithItemExpression()
{
Project project = new Project();
project.AddItem("i", "i1");
project.SetProperty("p1", "x@(i)x%(m)x");
Assert.Equal("x@(i)x%(m)x", project.GetPropertyValue("p1"));
}
/// <summary>
/// Setting a property to the same exact unevaluated and evaluated value
/// should not dirty the project.
/// (VS seems to do this a lot.)
/// </summary>
[Fact]
public void SetPropertyWithNoChangesShouldNotDirty()
{
Project project = new Project();
project.SetProperty("p", "v1");
Assert.True(project.IsDirty);
project.ReevaluateIfNecessary();
project.SetProperty("p", "v1");
Assert.False(project.IsDirty);
}
/// <summary>
/// Setting an evaluated property after its XML has been removed should
/// fail.
/// </summary>
[Fact]
public void SetPropertyAfterRemoved()
{
Assert.Throws<InvalidOperationException>(() =>
{
Project project = new Project();
var property = project.SetProperty("p", "v1");
property.Xml.Parent.RemoveAllChildren();
property.UnevaluatedValue = "v2";
});
}
/// <summary>
/// Setting an evaluated property after its XML's parent has been removed should
/// fail.
/// </summary>
[Fact]
public void SetPropertyAfterRemoved2()
{
Assert.Throws<InvalidOperationException>(() =>
{
Project project = new Project();
var property = project.SetProperty("p", "v1");
property.Xml.Parent.Parent.RemoveAllChildren();
property.UnevaluatedValue = "v2";
});
}
/// <summary>
/// Setting an evaluated metadatum after its XML has been removed should
/// fail.
/// </summary>
[Fact]
public void SetMetadatumAfterRemoved()
{
Assert.Throws<InvalidOperationException>(() =>
{
Project project = new Project();
var metadatum = project.AddItem("i", "i1")[0].SetMetadataValue("p", "v1");
metadatum.Xml.Parent.RemoveAllChildren();
metadatum.UnevaluatedValue = "v2";
});
}
/// <summary>
/// Changing an item's type after its XML has been removed should
/// fail.
/// </summary>
[Fact]
public void SetItemTypeAfterRemoved()
{
Assert.Throws<InvalidOperationException>(() =>
{
Project project = new Project();
var item = project.AddItem("i", "i1")[0];
item.Xml.Parent.RemoveAllChildren();
item.ItemType = "j";
});
}
/// <summary>
/// Changing an item's type after its XML has been removed should
/// fail.
/// </summary>
[Fact]
public void RemoveMetadataAfterItemRemoved()
{
Assert.Throws<InvalidOperationException>(() =>
{
Project project = new Project();
var item = project.AddItem("i", "i1")[0];
item.Xml.Parent.RemoveAllChildren();
item.RemoveMetadata("m");
});
}
[Theory]
[MemberData(nameof(ItemElementsThatRequireSplitting))]
public void RemoveMetadataThrowsWhenItemElementSplittingIsDisabled(string projectContents, int itemIndex, SetupProject setupProject)
{
AssertDisabledItemSplitting(projectContents, itemIndex, setupProject, (p, i) => { i.RemoveMetadata("bar"); }, "bar");
}
[Theory]
// explode on item coming from glob that expands to one item
[InlineData(
@"<Project ToolsVersion=""msbuilddefaulttoolsversion"" xmlns=""msbuildnamespace"">
<ItemGroup>
<a Include=""*.foo"">
{0}
</a>
</ItemGroup>
</Project>",
0, // operate on first item
new[] // files that should be captured by the glob
{
"a.foo"
})]
// explode on item coming from glob that expands to multiple items
[InlineData(
@"<Project ToolsVersion=""msbuilddefaulttoolsversion"" xmlns=""msbuildnamespace"">
<ItemGroup>
<a Include=""*.foo"">
{0}
</a>
</ItemGroup>
</Project>",
0,
new[]
{
"a.foo",
"b.foo"
})]
public void RemoveMetadataThrowsWhenItemElementSplittingIsDisabledAndItemComesFromGlob(string projectContents, int itemIndex, string[] files)
{
using (var env = TestEnvironment.Create())
{
var testProject = env.CreateTestProjectWithFiles(projectContents, files);
var projectFile = testProject.ProjectFile;
AssertDisabledItemSplitting(
projectContents,
itemIndex,
null,
(p, i) => { i.RemoveMetadata("bar"); },
"bar",
p =>
{
File.WriteAllText(projectFile, p);
return new Project(projectFile);
});
}
}
/// <summary>
/// Setting an evaluated metadatum after its XML's parent has been removed should
/// fail.
/// </summary>
[Fact]
public void SetMetadatumAfterRemoved2()
{
Assert.Throws<InvalidOperationException>(() =>
{
Project project = new Project();
var metadatum = project.AddItem("i", "i1")[0].SetMetadataValue("p", "v1");
metadatum.Xml.Parent.Parent.RemoveAllChildren();
metadatum.UnevaluatedValue = "v2";
});
}
/// <summary>
/// Setting an evaluated metadatum after its XML's parent's parent has been removed should
/// fail.
/// </summary>
[Fact]
public void SetMetadatumAfterRemoved3()
{
Assert.Throws<InvalidOperationException>(() =>
{
Project project = new Project();
var metadatum = project.AddItem("i", "i1")[0].SetMetadataValue("p", "v1");
metadatum.Xml.Parent.Parent.Parent.RemoveAllChildren();
metadatum.UnevaluatedValue = "v2";
});
}
[Theory]
[MemberData(nameof(ItemElementsThatRequireSplitting))]
[MemberData(nameof(ItemElementsWithGlobsThatRequireSplitting))]
public void SetMetadataThrowsWhenItemElementSplittingIsDisabled(string projectContents, int itemIndex, SetupProject setupProject)
{
AssertDisabledItemSplitting(projectContents, itemIndex, setupProject, (p, i) => { i.SetMetadataValue("foo", "bar"); });
}
/// <summary>
/// After removing an appropriate item group's XML without reevaluation an item is added;
/// it should go in a new one
/// </summary>
[Fact]
public void AddItemAfterAppropriateItemGroupRemoved()
{
Project project = new Project();
project.AddItem("i", "i1");
project.Xml.ItemGroups.First().Parent.RemoveAllChildren();
project.AddItem("i", "i2");
Assert.Single(project.Xml.Items);
project.ReevaluateIfNecessary();
Assert.Single(project.Items);
}
/// <summary>
/// Setting a property after an equivalent's XML has been removed without reevaluation, should
/// still work.
/// </summary>
[Fact]
public void SetNewPropertyAfterEquivalentRemoved()
{
Project project = new Project();
var property = project.SetProperty("p", "v1");
property.Xml.Parent.RemoveAllChildren();
project.SetProperty("p", "v2");
Assert.Single(project.Xml.Properties);
project.ReevaluateIfNecessary();
Assert.Equal("v2", project.GetPropertyValue("p"));
}
/// <summary>
/// Setting a property after an equivalent's XML's parent has been removed without reevaluation, should
/// still work.
/// </summary>
[Fact]
public void SetNewPropertyAfterEquivalentsParentRemoved()
{
Project project = new Project();
var property = project.SetProperty("p", "v1");
property.Xml.Parent.Parent.RemoveAllChildren();
project.SetProperty("p", "v2");
Assert.Single(project.Xml.Properties);
project.ReevaluateIfNecessary();
Assert.Equal("v2", project.GetPropertyValue("p"));
}
/// <summary>
/// Test removing a property. Parent empty group should also be removed.
/// </summary>
[Fact]
public void RemoveProperty()
{
Project project = new Project();
int environmentPropertyCount = Helpers.MakeList(project.Properties).Count;
project.SetProperty("p1", "v1");
project.ReevaluateIfNecessary();
project.RemoveProperty(project.GetProperty("p1"));
string expected = ObjectModelHelpers.CleanupFileContents(@"<Project ToolsVersion=""msbuilddefaulttoolsversion"" xmlns=""msbuildnamespace"" />");
Helpers.VerifyAssertProjectContent(expected, project.Xml);
Assert.Null(project.GetProperty("p1"));
ProjectInstance instance = project.CreateProjectInstance();
Assert.Equal(String.Empty, instance.GetPropertyValue("p1"));
Assert.Equal(0, Helpers.Count(project.Properties) - environmentPropertyCount);
}
/// <summary>
/// Test removing a property. Other property should not be disturbed.
/// </summary>
[Fact]
public void RemovePropertyWithSibling()
{
Project project = new Project();
project.SetProperty("p1", "v1");
project.SetProperty("p2", "v2");
project.ReevaluateIfNecessary();
project.RemoveProperty(project.GetProperty("p1"));
string expected = ObjectModelHelpers.CleanupFileContents(
@"<Project ToolsVersion=""msbuilddefaulttoolsversion"" xmlns=""msbuildnamespace"">
<PropertyGroup>
<p2>v2</p2>
</PropertyGroup>
</Project>");
Helpers.VerifyAssertProjectContent(expected, project.Xml);
}
/// <summary>
/// Add metadata to an existing item
/// </summary>
[Fact]
public void AddMetadata()
{
Project project = new Project();
ProjectItem item = project.AddItem("i", "i1")[0];
item.SetMetadataValue("m", "m1");
string expected = ObjectModelHelpers.CleanupFileContents(
@"<Project ToolsVersion=""msbuilddefaulttoolsversion"" xmlns=""msbuildnamespace"">
<ItemGroup>
<i Include=""i1"">
<m>m1</m>
</i>
</ItemGroup>
</Project>");
Helpers.VerifyAssertProjectContent(expected, project.Xml);
List<ProjectItem> items = Helpers.MakeList(project.Items);
Assert.Equal("m1", items[0].GetMetadataValue("m"));
Assert.Equal("m1", Helpers.MakeList(project.CreateProjectInstance().GetItems("i"))[0].GetMetadataValue("m"));
}
/// <summary>
/// Add metadata to an existing item
/// </summary>
[Fact]
public void AddMetadata_EscapedValue()
{
Project project = new Project();
ProjectItem item = project.AddItem("i", "i1")[0];
item.SetMetadataValue("m", "m1%24%24");
string expected = ObjectModelHelpers.CleanupFileContents(
@"<Project ToolsVersion=""msbuilddefaulttoolsversion"" xmlns=""msbuildnamespace"">
<ItemGroup>
<i Include=""i1"">
<m>m1%24%24</m>
</i>
</ItemGroup>
</Project>");
Helpers.VerifyAssertProjectContent(expected, project.Xml);
List<ProjectItem> items = Helpers.MakeList(project.Items);
Assert.Equal("m1$$", items[0].GetMetadataValue("m"));
Assert.Equal("m1$$", Helpers.MakeList(project.CreateProjectInstance().GetItems("i"))[0].GetMetadataValue("m"));
}
/// <summary>
/// Add metadata to an existing item that has existing metadata with that name.
/// Should replace it.
/// </summary>
[Fact]
public void AddMetadata_Existing()
{
Project project = new Project();
ProjectItem item = project.AddItem("i", "i1")[0];
ProjectMetadata metadatum1 = item.SetMetadataValue("m", "m1");
ProjectMetadata metadatum2 = item.SetMetadataValue("m", "m2");
string expected = ObjectModelHelpers.CleanupFileContents(
@"<Project ToolsVersion=""msbuilddefaulttoolsversion"" xmlns=""msbuildnamespace"">
<ItemGroup>
<i Include=""i1"">
<m>m2</m>
</i>
</ItemGroup>
</Project>");
Helpers.VerifyAssertProjectContent(expected, project.Xml);
List<ProjectItem> items = Helpers.MakeList(project.Items);
Assert.Equal("m2", items[0].GetMetadataValue("m"));
Assert.True(object.ReferenceEquals(metadatum1, metadatum2));
}
/// <summary>
/// Add an item whose include expands to several items.
/// Even without reevaluation, we should get two items.
/// </summary>
[Fact]
public void AddItem_ExpandsToSeveral()
{
Project project = new Project();
IList<ProjectItem> items = project.AddItem("i", "a;b");
Assert.True(object.ReferenceEquals(items[0].Xml, items[1].Xml));
Assert.Equal("a;b", items[0].UnevaluatedInclude);
items = Helpers.MakeList(project.Items);
Assert.Equal("a", items[0].EvaluatedInclude);
Assert.Equal("b", items[1].EvaluatedInclude);
}
/// <summary>
/// Add an item expanding to several, with metadata
/// </summary>
[Fact]
public void AddItem_ExpandsToSeveralWithMetadata()
{
Project project = new Project();
List<KeyValuePair<string, string>> metadata = new List<KeyValuePair<string, string>>();
metadata.Add(new KeyValuePair<string, string>("m", "m1"));
project.AddItem("i", "i1;i2", metadata);
string expected = ObjectModelHelpers.CleanupFileContents(
@"<Project ToolsVersion=""msbuilddefaulttoolsversion"" xmlns=""msbuildnamespace"">
<ItemGroup>
<i Include=""i1"">
<m>m1</m>
</i>
<i Include=""i2"">
<m>m1</m>
</i>
</ItemGroup>
</Project>");
Helpers.VerifyAssertProjectContent(expected, project.Xml);
}
/// <summary>
/// Add metadata that would be modified by evaluation.
/// Should be evaluated on a best-effort basis.
/// </summary>
[Fact]
public void AddMetadata_Reevaluation()
{
var content = ObjectModelHelpers.CleanupFileContents(
@"<Project ToolsVersion=""msbuilddefaulttoolsversion"" xmlns=""msbuildnamespace"">
<ItemGroup>
<i Include=""i1"">
<l>l1</l>
<m>m1</m>
</i>
</ItemGroup>
</Project>");
using ProjectFromString projectFromString = new(content);
Project project = projectFromString.Project;
ProjectItem item = Helpers.GetFirst(project.Items);
item.SetMetadataValue("m", "%(l)");
Assert.Equal("l1", item.GetMetadata("m").EvaluatedValue);
Assert.Equal("%(l)", item.GetMetadata("m").Xml.Value);
string expected = ObjectModelHelpers.CleanupFileContents(
@"<Project ToolsVersion=""msbuilddefaulttoolsversion"" xmlns=""msbuildnamespace"">
<ItemGroup>
<i Include=""i1"">
<l>l1</l>
<m>%(l)</m>
</i>
</ItemGroup>
</Project>");
Helpers.VerifyAssertProjectContent(expected, project.Xml, false);
project.ReevaluateIfNecessary();
item = Helpers.GetFirst(project.Items);
Assert.Equal("l1", item.GetMetadata("m").EvaluatedValue);
Assert.Equal("%(l)", item.GetMetadata("m").Xml.Value);
}
/// <summary>
/// Add a new piece of item definition metadatum and update an existing one.
/// The new piece has to go in an entirely new item definition.
/// </summary>
[Fact]
public void AddMetadatumToItemDefinition()
{
ProjectRootElement xml = ProjectRootElement.Create();
xml.AddItemDefinitionGroup().AddItemDefinition("i").AddMetadata("m", "m0");
Project project = new Project(xml);
ProjectItemDefinition definition = project.ItemDefinitions["i"];
definition.SetMetadataValue("m", "m1");
definition.SetMetadataValue("n", "n0");
string expected = ObjectModelHelpers.CleanupFileContents(
@"<Project ToolsVersion=""msbuilddefaulttoolsversion"" xmlns=""msbuildnamespace"">
<ItemDefinitionGroup>
<i>
<m>m1</m>
</i>
<i>
<n>n0</n>
</i>
</ItemDefinitionGroup>
</Project>");
Helpers.VerifyAssertProjectContent(expected, project.Xml);
}
/// <summary>
/// Add an item to an empty project
/// </summary>
[Fact]
public void AddItemFast()
{
Project project = new Project();
project.AddItemFast("i", "i1");
string expected = ObjectModelHelpers.CleanupFileContents(
@"<Project ToolsVersion=""msbuilddefaulttoolsversion"" xmlns=""msbuildnamespace"">
<ItemGroup>
<i Include=""i1"" />
</ItemGroup>
</Project>");
Helpers.VerifyAssertProjectContent(expected, project.Xml);
List<ProjectItem> items = Helpers.MakeList(project.Items);
Assert.Single(items);
Assert.Equal("i", items[0].ItemType);
Assert.Equal("i1", items[0].EvaluatedInclude);
Assert.Equal("i1", Helpers.GetFirst(project.GetItems("i")).EvaluatedInclude);
Assert.Equal("i1", Helpers.MakeList(project.CreateProjectInstance().GetItems("i"))[0].EvaluatedInclude);
}
/// <summary>
/// Add an item to an empty project, where the include is escaped
/// </summary>
[Fact]
public void AddItemFast_EscapedItemInclude()
{
Project project = new Project();
project.AddItemFast("i", "i%281%29");
string expected = ObjectModelHelpers.CleanupFileContents(
@"<Project ToolsVersion=""msbuilddefaulttoolsversion"" xmlns=""msbuildnamespace"">
<ItemGroup>
<i Include=""i%281%29"" />
</ItemGroup>
</Project>");
Helpers.VerifyAssertProjectContent(expected, project.Xml);
List<ProjectItem> items = Helpers.MakeList(project.Items);
Assert.Single(items);
Assert.Equal("i", items[0].ItemType);
Assert.Equal("i(1)", items[0].EvaluatedInclude);
Assert.Equal("i(1)", Helpers.GetFirst(project.GetItems("i")).EvaluatedInclude);
Assert.Equal("i(1)", Helpers.MakeList(project.CreateProjectInstance().GetItems("i"))[0].EvaluatedInclude);
}
/// <summary>
/// Add an item with metadata
/// </summary>
[Fact]
public void AddItemFast_WithMetadata()
{
Project project = new Project();
List<KeyValuePair<string, string>> metadata = new List<KeyValuePair<string, string>>();
metadata.Add(new KeyValuePair<string, string>("m", "m1"));
project.AddItemFast("i", "i1", metadata);
string expected = ObjectModelHelpers.CleanupFileContents(
@"<Project ToolsVersion=""msbuilddefaulttoolsversion"" xmlns=""msbuildnamespace"">
<ItemGroup>
<i Include=""i1"">
<m>m1</m>
</i>
</ItemGroup>
</Project>");
Helpers.VerifyAssertProjectContent(expected, project.Xml);
}
/// <summary>
/// Add an item with empty include.
/// Should throw.
/// </summary>
[Fact]
public void AddItemFast_InvalidEmptyInclude()
{
Assert.Throws<ArgumentException>(() =>
{
Project project = new Project();
project.AddItemFast("i", String.Empty);
});
}
/// <summary>
/// Add an item with null metadata parameter.
/// Should just add no metadata.
/// </summary>
[Fact]
public void AddItemFast_NullMetadata()
{
Project project = new Project();
project.AddItemFast("i", "i1", null);
string expected = ObjectModelHelpers.CleanupFileContents(
@"<Project ToolsVersion=""msbuilddefaulttoolsversion"" xmlns=""msbuildnamespace"">
<ItemGroup>
<i Include=""i1"" />
</ItemGroup>
</Project>");
Helpers.VerifyAssertProjectContent(expected, project.Xml);
}
/// <summary>
/// Add an item whose include has a property expression. As a convenience, we attempt to expand the
/// expression to create the evaluated include.
/// </summary>
[Fact]
public void AddItemFast_IncludeContainsPropertyExpression()
{
Project project = new Project();
project.SetProperty("p", "v1");
project.ReevaluateIfNecessary();
project.AddItemFast("i", "$(p)");
Assert.Equal("$(p)", Helpers.GetFirst(project.Items).UnevaluatedInclude);
Assert.Equal("v1", Helpers.GetFirst(project.Items).EvaluatedInclude);
}
/// <summary>
/// Add an item whose include has a wildcard. We attempt to expand the wildcard using the
/// file system. In this case, we have one entry in the project and two evaluated items.
/// </summary>
[Fact]
public void AddItemFast_IncludeContainsWildcard()
{
string[] paths = null;
try
{
paths = Helpers.CreateFiles("i1.xxx", "i2.xxx");
string wildcard = Path.Combine(Path.GetDirectoryName(paths[0]), "*.xxx;");
Project project = new Project();
project.AddItemFast("i", wildcard);
List<ProjectItem> items = Helpers.MakeList(project.Items);
Assert.Equal(2, items.Count);
Assert.Equal(paths[0], items[0].EvaluatedInclude);
Assert.Equal(paths[1], items[1].EvaluatedInclude);
}
finally
{
Helpers.DeleteFiles(paths);
}
}
/// <summary>
/// Add an item whose include has an item expression. As a convenience, we attempt to expand the
/// expression to create the evaluated include.
/// This value will not be reliable until the project is reevaluated --
/// for example, it assumes any items referenced are defined above this one.
/// </summary>
[Fact]
public void AddItemFast_IncludeContainsItemExpression()
{
Project project = new Project();
project.AddItemFast("h", "h1");
project.ReevaluateIfNecessary();
ProjectItem item = project.AddItemFast("i", "@(h)")[0];
Assert.Equal("@(h)", item.UnevaluatedInclude);
Assert.Equal("h1", item.EvaluatedInclude);
}
/// <summary>
/// Add an item whose include contains a wildcard but doesn't match anything.
/// </summary>
[Fact]
public void AddItemFast_ContainingWildcardNoMatches()
{
Project project = new Project();
IList<ProjectItem> items = project.AddItemFast("i",
NativeMethodsShared.IsWindows ? @"c:\" + Guid.NewGuid().ToString() + @"\**\i1" : "/" + Guid.NewGuid().ToString() + "/**/i1");
Assert.Empty(items);
}
/// <summary>
/// Add an item whose include contains a wildcard.
/// In this case we don't try to reuse an existing wildcard expression.
/// </summary>
[Fact]
public void AddItemFast_ContainingWildcardExistingWildcard()
{
Project project = new Project();
project.Xml.AddItem("i", "*.xxx");
project.ReevaluateIfNecessary();
project.AddItemFast("i", "*.xxx");
string expected = ObjectModelHelpers.CleanupFileContents(
@"<Project ToolsVersion=""msbuilddefaulttoolsversion"" xmlns=""msbuildnamespace"">
<ItemGroup>
<i Include=""*.xxx"" />
<i Include=""*.xxx"" />
</ItemGroup>
</Project>");
Helpers.VerifyAssertProjectContent(expected, project);
}
/// <summary>
/// Add an item whose include contains a semicolon.
/// In this case we don't try to reuse an existing wildcard expression.
/// </summary>
[Fact]
public void AddItemFast_ContainingSemicolonExistingWildcard()
{
Project project = new Project();
project.Xml.AddItem("i", "*.xxx");
project.ReevaluateIfNecessary();
project.AddItemFast("i", "i1.xxx;i2.xxx");
string expected = ObjectModelHelpers.CleanupFileContents(
@"<Project ToolsVersion=""msbuilddefaulttoolsversion"" xmlns=""msbuildnamespace"">
<ItemGroup>
<i Include=""*.xxx"" />
<i Include=""i1.xxx;i2.xxx"" />
</ItemGroup>
</Project>");
Helpers.VerifyAssertProjectContent(expected, project);
}
/// <summary>
/// If user tries to add a new item that has the same item name as an existing
/// wildcarded item, but the wildcard won't pick up the new file, then we
/// of course have to add the new item.
/// </summary>
[Fact]
public void AddItemFast_DoesntMatchWildcard()
{
Project project = new Project();
project.Xml.AddItem("i", "*.xxx");
project.ReevaluateIfNecessary();
project.AddItemFast("i", "i1");
string expected = ObjectModelHelpers.CleanupFileContents(
@"<Project ToolsVersion=""msbuilddefaulttoolsversion"" xmlns=""msbuildnamespace"">
<ItemGroup>
<i Include=""*.xxx"" />
<i Include=""i1"" />
</ItemGroup>
</Project>");
Helpers.VerifyAssertProjectContent(expected, project);
}
/// <summary>
/// When the wildcarded item already in the project file has a Condition
/// on it, we don't try to match with it when a user tries to add a new
/// item to the project.
/// </summary>
[Fact]
public void AddItemFast_MatchesWildcardWithCondition()
{
Project project = new Project();
ProjectItemElement itemElement = project.Xml.AddItem("i", "*.xxx");
itemElement.Condition = "true";
project.ReevaluateIfNecessary();
project.AddItemFast("i", "i1.xxx");
string expected = ObjectModelHelpers.CleanupFileContents(
@"<Project ToolsVersion=""msbuilddefaulttoolsversion"" xmlns=""msbuildnamespace"">
<ItemGroup>
<i Include=""*.xxx"" Condition=""true"" />
<i Include=""i1.xxx"" />
</ItemGroup>
</Project>");
Helpers.VerifyAssertProjectContent(expected, project);
}
/// <summary>
/// When the wildcarded item already in the project file has a Exclude
/// on it, we don't try to match with it when a user tries to add a new
/// item to the project.
/// </summary>
[Fact]
public void AddItemFast_MatchesWildcardWithExclude()
{
Project project = new Project();
ProjectItemElement itemElement = project.Xml.AddItem("i", "*.xxx");
itemElement.Exclude = "i2.xxx";
project.ReevaluateIfNecessary();
project.AddItemFast("i", "i1.xxx");
string expected = ObjectModelHelpers.CleanupFileContents(
@"<Project ToolsVersion=""msbuilddefaulttoolsversion"" xmlns=""msbuildnamespace"">
<ItemGroup>
<i Include=""*.xxx"" Exclude=""i2.xxx"" />
<i Include=""i1.xxx"" />
</ItemGroup>
</Project>");
Helpers.VerifyAssertProjectContent(expected, project);
}
/// <summary>
/// There's a wildcard in the project already, and the user tries to add an item
/// that matches that wildcard. In this case, we don't touch the project at all.
/// </summary>
[Fact]
public void AddItemFast_MatchesWildcard()
{
Project project = new Project();
ProjectItemElement item1 = project.Xml.AddItem("i", "*.xxx");
project.ReevaluateIfNecessary();
ProjectItemElement item2 = project.AddItemFast("i", "i1.xxx")[0].Xml;
string expected = ObjectModelHelpers.CleanupFileContents(
@"<Project ToolsVersion=""msbuilddefaulttoolsversion"" xmlns=""msbuildnamespace"">
<ItemGroup>
<i Include=""*.xxx"" />
</ItemGroup>
</Project>");
Helpers.VerifyAssertProjectContent(expected, project);
Assert.True(object.ReferenceEquals(item1, item2));
}
/// <summary>
/// There's a wildcard in the project already, and the user tries to add an item
/// that matches that wildcard, except that its item type is different.
/// In this case, we ignore the existing wildcard.
/// </summary>
[Fact]
public void AddItemFast_MatchesWildcardButNotItemType()
{
Project project = new Project();
project.Xml.AddItem("i", "*.xxx");
project.ReevaluateIfNecessary();
project.AddItemFast("j", "j1.xxx");
string expected = ObjectModelHelpers.CleanupFileContents(
@"<Project ToolsVersion=""msbuilddefaulttoolsversion"" xmlns=""msbuildnamespace"">
<ItemGroup>
<i Include=""*.xxx"" />
</ItemGroup>
<ItemGroup>
<j Include=""j1.xxx"" />
</ItemGroup>
</Project>");
Helpers.VerifyAssertProjectContent(expected, project);
}
/// <summary>
/// There's a complicated recursive wildcard in the project already, and the user tries to add an item
/// that matches that wildcard. In this case, we don't touch the project at all.
/// </summary>
[Fact]
public void AddItemFast_MatchesComplicatedWildcard()
{
Project project = new Project();
ProjectItemElement item1 = project.Xml.AddItem("i",
NativeMethodsShared.IsWindows ? @"c:\subdir1\**\subdir2\**\*.x?x" : "/subdir1/**/subdir2/**/*.x?x");
project.ReevaluateIfNecessary();
ProjectItemElement item2 = project.AddItemFast("i",
NativeMethodsShared.IsWindows ? @"c:\subdir1\a\b\subdir2\c\i1.xyx" : "/subdir1/a/b/subdir2/c/i1.xyx")[0].Xml;
string expected = ObjectModelHelpers.CleanupFileContents(
NativeMethodsShared.IsWindows ?
@"<Project ToolsVersion=""msbuilddefaulttoolsversion"" xmlns=""msbuildnamespace"">
<ItemGroup>
<i Include=""c:\subdir1\**\subdir2\**\*.x?x"" />
</ItemGroup>
</Project>" :
@"<Project ToolsVersion=""msbuilddefaulttoolsversion"" xmlns=""msbuildnamespace"">
<ItemGroup>
<i Include=""/subdir1/**/subdir2/**/*.x?x"" />
</ItemGroup>
</Project>");
Helpers.VerifyAssertProjectContent(expected, project);
Assert.True(object.ReferenceEquals(item1, item2));
}
/// <summary>
/// There's a complicated recursive wildcard in the project already, and the user tries to add an item
/// that doesn't match that wildcard. In this case, we don't touch the project at all.
/// </summary>
[Fact]
public void AddItemFast_DoesntMatchComplicatedWildcard()
{
Project project = new Project();
ProjectItemElement item1 = project.Xml.AddItem("i", @"c:\subdir1\**\subdir2\**\*.x?x");
project.ReevaluateIfNecessary();
ProjectItemElement item2 = project.AddItemFast("i", @"c:\subdir1\a\b\c\i1.xyx")[0].Xml;
string expected = ObjectModelHelpers.CleanupFileContents(
@"<Project ToolsVersion=""msbuilddefaulttoolsversion"" xmlns=""msbuildnamespace"">
<ItemGroup>
<i Include=""c:\subdir1\**\subdir2\**\*.x?x"" />
<i Include=""c:\subdir1\a\b\c\i1.xyx"" />
</ItemGroup>
</Project>");
Helpers.VerifyAssertProjectContent(expected, project);
Assert.False(object.ReferenceEquals(item1, item2));
}
/// <summary>
/// There's a wildcard in the project already, and the user tries to add an item
/// that matches that wildcard. In this case, we add a new item, because the old
/// one wasn't equivalent.
/// In contrast Orcas/Whidbey assumed that the user wants
/// that metadata on the new item, too.
/// </summary>
[Fact]
public void AddItemFast_DoesNotMatchWildcardWithMetadata()
{
Project project = new Project();
ProjectItemElement item1 = project.Xml.AddItem("i", "*.xxx");
item1.AddMetadata("m", "m1");
project.ReevaluateIfNecessary();
project.AddItemFast("i", "i1.xxx");
string expected = ObjectModelHelpers.CleanupFileContents(
@"<Project ToolsVersion=""msbuilddefaulttoolsversion"" xmlns=""msbuildnamespace"">
<ItemGroup>
<i Include=""*.xxx"">
<m>m1</m>
</i>
<i Include=""i1.xxx"" />
</ItemGroup>
</Project>");
Helpers.VerifyAssertProjectContent(expected, project);
}
/// <summary>
/// There's a wildcard in the project already, and the user tries to add an item
/// with metadata. In this case, we add a new item, because the old
/// one wasn't equivalent.
/// </summary>
[Fact]
public void AddItemFastWithMetadata_DoesNotMatchWildcardWithNoMetadata()
{
Project project = new Project();
project.Xml.AddItem("i", "*.xxx");
project.ReevaluateIfNecessary();
Dictionary<string, string> metadata = new Dictionary<string, string>() { { "m", "m1" } };
project.AddItemFast("i", "i1.xxx", metadata);
string expected = ObjectModelHelpers.CleanupFileContents(
@"<Project ToolsVersion=""msbuilddefaulttoolsversion"" xmlns=""msbuildnamespace"">
<ItemGroup>
<i Include=""*.xxx"" />
<i Include=""i1.xxx"">
<m>m1</m>
</i>
</ItemGroup>
</Project>");
Helpers.VerifyAssertProjectContent(expected, project);
}
/// <summary>
/// There's a wildcard in the project already, but it's part of a semicolon-separated
/// list of items. Now the user tries to add an item that matches that wildcard.
/// In this case, we don't touch the project at all.
/// </summary>
[Fact]
public void AddItemFast_MatchesWildcardInSemicolonList()
{
Project project = new Project();
project.Xml.AddItem("i", "a;*.xxx;b");
project.ReevaluateIfNecessary();
project.AddItemFast("i", "i1.xxx");
string expected = ObjectModelHelpers.CleanupFileContents(
@"<Project ToolsVersion=""msbuilddefaulttoolsversion"" xmlns=""msbuildnamespace"">
<ItemGroup>
<i Include=""a;*.xxx;b"" />
</ItemGroup>
</Project>");
Helpers.VerifyAssertProjectContent(expected, project);
}
private static void AssertDisabledItemSplitting(string projectContents, int itemIndex, SetupProject setupProject, Action<Project, ProjectItem> itemOperation, string metadataToInsert = "", Func<string, Project> projectProvider = null)
{
var metadataElement = string.IsNullOrEmpty(metadataToInsert)
? ""
: $"<{metadataToInsert}>metadata</{metadataToInsert}>";
projectContents = string.Format(projectContents, metadataElement);
projectContents = ObjectModelHelpers.CleanupFileContents(projectContents);
Project project;
if (projectProvider != null)
{
project = projectProvider(projectContents);
}
else
{
using ProjectFromString projectFromString = new(projectContents);
project = projectFromString.Project;
setupProject?.Invoke(project);
project.ReevaluateIfNecessary();
}
project.ThrowInsteadOfSplittingItemElement = true;
var initialXml = project.Xml.RawXml;
var item = project.Items.ElementAt(itemIndex);
var ex = Assert.Throws<InvalidOperationException>(() => itemOperation(project, item));
Assert.Matches("The requested operation needs to split the item element at location .* into individual elements but item element splitting is disabled with .*", ex.Message);
Assert.False(project.IsDirty, "project should not be dirty after item splitting threw exception");
Assert.Equal(initialXml, project.Xml.RawXml);
}
}
}
|