File: Definition\ProjectItemDefinition_Tests.cs
Web Access
Project: ..\..\..\src\Build.OM.UnitTests\Microsoft.Build.Engine.OM.UnitTests.csproj (Microsoft.Build.Engine.OM.UnitTests)
// 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.Framework;
using Microsoft.Build.Shared;
using Xunit;
using InvalidProjectFileException = Microsoft.Build.Exceptions.InvalidProjectFileException;
 
#nullable disable
 
namespace Microsoft.Build.UnitTests.OM.Definition
{
    /// <summary>
    /// Tests for ProjectItemDefinition
    /// </summary>
    public class ProjectItemDefinition_Tests
    {
        /// <summary>
        /// Add metadata; should add to an existing item definition group that has item definitions of the same item type
        /// </summary>
        [Fact]
        public void AddMetadataExistingItemDefinitionGroup()
        {
            ProjectRootElement xml = ProjectRootElement.Create();
            xml.AddItemDefinitionGroup().AddItemDefinition("i").AddMetadata("m", "m0");
 
            Project project = new Project(xml);
            project.ItemDefinitions["i"].SetMetadataValue("n", "n0");
 
            string expected = ObjectModelHelpers.CleanupFileContents(
@"<Project ToolsVersion=""msbuilddefaulttoolsversion"" xmlns=""msbuildnamespace"">
  <ItemDefinitionGroup>
    <i>
      <m>m0</m>
    </i>
    <i>
      <n>n0</n>
    </i>
  </ItemDefinitionGroup>
</Project>");
 
            Helpers.VerifyAssertProjectContent(expected, project.Xml);
        }
 
        /// <summary>
        /// Set metadata with property expression; should be expanded
        /// </summary>
        [Fact]
        public void SetMetadata()
        {
            ProjectRootElement xml = ProjectRootElement.Create();
            xml.AddProperty("p", "v");
            xml.AddItemDefinitionGroup().AddItemDefinition("i").AddMetadata("m", "m0");
            xml.AddItem("i", "i1");
 
            Project project = new Project(xml);
 
            ProjectMetadata metadatum = project.ItemDefinitions["i"].GetMetadata("m");
 
            metadatum.UnevaluatedValue = "$(p)";
 
            Assert.Equal("v", Helpers.GetFirst(project.GetItems("i")).GetMetadataValue("m"));
        }
 
        /// <summary>
        /// Access metadata when there isn't any
        /// </summary>
        [Fact]
        public void EmptyMetadataCollection()
        {
            ProjectRootElement xml = ProjectRootElement.Create();
            xml.AddItemDefinitionGroup().AddItemDefinition("i");
            Project project = new Project(xml);
 
            ProjectItemDefinition itemDefinition = project.ItemDefinitions["i"];
            IEnumerable<ProjectMetadata> metadataCollection = itemDefinition.Metadata;
 
            List<ProjectMetadata> metadataList = Helpers.MakeList(metadataCollection);
 
            Assert.Empty(metadataList);
 
            Assert.Null(itemDefinition.GetMetadata("m"));
        }
 
        /// <summary>
        /// Set metadata get collection
        /// </summary>
        [Fact]
        public void GetMetadataCollection()
        {
            ProjectRootElement xml = ProjectRootElement.Create();
            xml.AddItemDefinitionGroup().AddItemDefinition("i").AddMetadata("m", "m0");
 
            Project project = new Project(xml);
 
            IEnumerable<ProjectMetadata> metadataCollection = project.ItemDefinitions["i"].Metadata;
 
            List<ProjectMetadata> metadataList = Helpers.MakeList(metadataCollection);
 
            Assert.Single(metadataList);
            Assert.Equal("m", metadataList[0].Name);
            Assert.Equal("m0", metadataList[0].EvaluatedValue);
        }
 
        /// <summary>
        /// Attempt to update metadata on imported item definition should fail
        /// </summary>
        [Fact]
        public void UpdateMetadataImported()
        {
            Assert.Throws<InvalidOperationException>(() =>
            {
                string file = null;
 
                try
                {
                    file = Microsoft.Build.Shared.FileUtilities.GetTemporaryFile();
                    ProjectRootElement import = ProjectRootElement.Create(file);
                    import.AddItemDefinitionGroup().AddItemDefinition("i").AddMetadata("m", "m0");
                    import.Save();
 
                    ProjectRootElement main = ProjectRootElement.Create();
                    Project project = new Project(main);
                    main.AddImport(file);
                    project.ReevaluateIfNecessary();
 
                    ProjectItemDefinition definition = project.ItemDefinitions["i"];
                    definition.SetMetadataValue("m", "m1");
                }
                finally
                {
                    File.Delete(file);
                }
            });
        }
        /// <summary>
        /// Attempt to add new metadata on imported item definition should succeed,
        /// creating a new item definition in the main project
        /// </summary>
        [Fact]
        public void SetMetadataImported()
        {
            string file = null;
 
            try
            {
                file = Microsoft.Build.Shared.FileUtilities.GetTemporaryFile();
                ProjectRootElement import = ProjectRootElement.Create(file);
                import.AddItemDefinitionGroup().AddItemDefinition("i").AddMetadata("m", "m0");
                import.Save();
 
                ProjectRootElement main = ProjectRootElement.Create();
                Project project = new Project(main);
                main.AddImport(file);
                project.ReevaluateIfNecessary();
 
                ProjectItemDefinition definition = project.ItemDefinitions["i"];
                definition.SetMetadataValue("n", "n0");
 
                string expected = String.Format(
    ObjectModelHelpers.CleanupFileContents(
@"<Project ToolsVersion=""msbuilddefaulttoolsversion"" xmlns=""msbuildnamespace"">
  <ItemDefinitionGroup>
    <i>
      <n>n0</n>
    </i>
  </ItemDefinitionGroup>
  <Import Project=""{0}"" />
</Project>"),
                   file);
 
                Helpers.VerifyAssertProjectContent(expected, project.Xml);
            }
            finally
            {
                File.Delete(file);
            }
        }
 
        /// <summary>
        /// Item definition metadata should be sufficient to avoid errors like
        /// "error MSB4096: The item "a.foo" in item list "h" does not define a value for metadata "m".  In
        /// order to use this metadata, either qualify it by specifying %(h.m), or ensure that all items in this list define a value
        /// for this metadata."
        /// </summary>
        [Fact]
        [Trait("Category", "serialize")]
        public void BatchingConsidersItemDefinitionMetadata()
        {
            string content =
ObjectModelHelpers.CleanupFileContents(
@"<Project ToolsVersion=""msbuilddefaulttoolsversion"" xmlns=""msbuildnamespace"">
  <ItemDefinitionGroup>
    <i>
      <m>m1</m>
    </i>
  </ItemDefinitionGroup>
  <ItemGroup>
    <i Include='a.foo;a.bar'/>
  </ItemGroup>
  <Target Name='t'>
    <Message Text='@(i)/%(m)'/>
  </Target>
</Project>");
 
            using ProjectFromString projectFromString = new(content);
            Project project = projectFromString.Project;
 
            MockLogger logger = new MockLogger();
            List<ILogger> loggers = new List<ILogger>() { logger };
            Assert.True(project.Build(loggers));
 
            logger.AssertLogContains("a.foo;a.bar/m1");
            logger.AssertNoErrors();
            logger.AssertNoWarnings();
        }
 
        /// <summary>
        /// Expand built-in metadata "late"
        /// </summary>
        [Fact]
        public void ExpandBuiltInMetadataAtPointOfUse()
        {
            string content =
ObjectModelHelpers.CleanupFileContents(
@"<Project ToolsVersion=""msbuilddefaulttoolsversion"" xmlns=""msbuildnamespace"">
  <ItemDefinitionGroup>
    <i>
      <m>%(filename)</m>
    </i>
  </ItemDefinitionGroup>
  <ItemGroup>
    <i Include='" + (NativeMethodsShared.IsWindows ? @"c:\a\b.ext" : "/a/b.ext") + @"'/>
  </ItemGroup>
</Project>");
 
            using ProjectFromString projectFromString = new(content);
            Project project = projectFromString.Project;
 
            ProjectItem item = project.GetItems("i").ElementAt(0);
            Assert.Equal("b", item.GetMetadataValue("m"));
        }
 
        /// <summary>
        /// Expand built-in metadata "late"
        /// </summary>
        [Fact]
        public void ExpandBuiltInMetadataAtPointOfUse_ReferToMetadataAbove()
        {
            string content =
ObjectModelHelpers.CleanupFileContents(
@"<Project ToolsVersion=""msbuilddefaulttoolsversion"" xmlns=""msbuildnamespace"">
  <ItemDefinitionGroup>
    <i>
      <m>%(filename)</m>
      <m>%(m)%(extension)</m>
    </i>
  </ItemDefinitionGroup>
  <ItemGroup>
    <i Include='" + (NativeMethodsShared.IsWindows ? @"c:\a\b.ext" : "/a/b.ext") + @"'/>
  </ItemGroup>
</Project>");
 
            using ProjectFromString projectFromString = new(content);
            Project project = projectFromString.Project;
 
            ProjectItem item = project.GetItems("i").ElementAt(0);
            Assert.Equal("b.ext", item.GetMetadataValue("m"));
        }
 
        /// <summary>
        /// Expand built-in metadata "late"
        /// </summary>
        [Fact]
        public void ExpandBuiltInMetadataAtPointOfUse_MixtureOfCustomAndBuiltIn()
        {
            string content =
ObjectModelHelpers.CleanupFileContents(
@"<Project ToolsVersion=""msbuilddefaulttoolsversion"" xmlns=""msbuildnamespace"">
  <ItemDefinitionGroup>
    <i>
      <l>l1</l>
      <m>%(filename).%(l)</m>
    </i>
  </ItemDefinitionGroup>
  <ItemGroup>
    <i Include='" + (NativeMethodsShared.IsWindows ? @"c:\a\b.ext" : "/a/b.ext") + @"'/>
  </ItemGroup>
</Project>");
 
            using ProjectFromString projectFromString = new(content);
            Project project = projectFromString.Project;
 
            ProjectItem item = project.GetItems("i").ElementAt(0);
            Assert.Equal("b.l1", item.GetMetadataValue("m"));
        }
 
        /// <summary>
        /// Custom metadata expressions on metadata on an ItemDefinitionGroup is still always
        /// expanded right there.
        /// </summary>
        [Fact]
        public void ExpandBuiltInMetadataAtPointOfUse_CustomEvaluationNeverDelayed()
        {
            string content =
ObjectModelHelpers.CleanupFileContents(
@"<Project ToolsVersion=""msbuilddefaulttoolsversion"" xmlns=""msbuildnamespace"">
  <ItemDefinitionGroup>
    <i>
      <n>n1</n>
      <m>%(filename).%(n)</m>
      <n>n2</n>
    </i>
  </ItemDefinitionGroup>
  <ItemGroup>
    <i Include='" + (NativeMethodsShared.IsWindows ? @"c:\a\b.ext" : "/a/b.ext") + @"'>
      <n>n3</n>
    </i>
  </ItemGroup>
</Project>");
 
            using ProjectFromString projectFromString = new(content);
            Project project = projectFromString.Project;
 
            ProjectItem item = project.GetItems("i").ElementAt(0);
            Assert.Equal("b.n1", item.GetMetadataValue("m"));
        }
 
        /// <summary>
        /// A custom metadata that bizarrely expands to a built in metadata expression should
        /// not evaluate again.
        /// </summary>
        [Fact]
        public void ExpandBuiltInMetadataAtPointOfUse_DoNotDoubleEvaluate()
        {
            string content =
ObjectModelHelpers.CleanupFileContents(
@"<Project ToolsVersion=""msbuilddefaulttoolsversion"" xmlns=""msbuildnamespace"">
  <ItemDefinitionGroup>
    <i>
      <n>%25(filename)</n> <!-- escaped % sign -->
      <m>%(n)</m>
    </i>
  </ItemDefinitionGroup>
  <ItemGroup>
    <i Include='c:\a\b.ext'/>
  </ItemGroup>
</Project>");
 
            using ProjectFromString projectFromString = new(content);
            Project project = projectFromString.Project;
 
            ProjectItem item = project.GetItems("i").ElementAt(0);
 
            Assert.Equal("%25(filename)", Project.GetMetadataValueEscaped(item, "m"));
            Assert.Equal("%(filename)", item.GetMetadataValue("m"));
        }
 
        /// <summary>
        /// Items created from other items should still have the built-in metadata expanded
        /// on them, not the original items.
        /// </summary>
        [Fact]
        public void ExpandBuiltInMetadataAtPointOfUse_CopyItems()
        {
            string content =
ObjectModelHelpers.CleanupFileContents(
@"<Project ToolsVersion=""msbuilddefaulttoolsversion"" xmlns=""msbuildnamespace"">
  <ItemDefinitionGroup>
    <i>
      <m>%(extension)</m>
    </i>
  </ItemDefinitionGroup>
  <ItemGroup>
    <h Include='a.foo'/>
    <i Include=""@(h->'%(identity).bar')""/>
  </ItemGroup>
</Project>");
 
            using ProjectFromString projectFromString = new(content);
            Project project = projectFromString.Project;
 
            ProjectItem item = project.GetItems("i").ElementAt(0);
            Assert.Equal(".bar", item.GetMetadataValue("m"));
        }
 
        /// <summary>
        /// Items created from other items should still have the built-in metadata expanded
        /// on them, not the original items.
        /// </summary>
        [Fact]
        public void ExpandBuiltInMetadataAtPointOfUse_UseInTransform()
        {
            string content =
ObjectModelHelpers.CleanupFileContents(
@"<Project ToolsVersion=""msbuilddefaulttoolsversion"" xmlns=""msbuildnamespace"">
  <ItemDefinitionGroup>
    <h>
      <m>%(extension)</m>
    </h>
  </ItemDefinitionGroup>
  <ItemGroup>
    <h Include='a.foo'/>
    <i Include=""@(h->'%(m)')""/>
  </ItemGroup>
</Project>");
 
            using ProjectFromString projectFromString = new(content);
            Project project = projectFromString.Project;
 
            ProjectItem item = project.GetItems("i").ElementAt(0);
            Assert.Equal(".foo", item.EvaluatedInclude);
        }
 
        /// <summary>
        /// Items created from other items should still have the built-in metadata expanded
        /// on them, not the original items.
        /// </summary>
        [Fact]
        [Trait("Category", "serialize")]
        public void ExpandBuiltInMetadataAtPointOfUse_UseInBatching()
        {
            string content =
ObjectModelHelpers.CleanupFileContents(
@"<Project ToolsVersion=""msbuilddefaulttoolsversion"" xmlns=""msbuildnamespace"">
  <ItemDefinitionGroup>
    <h>
      <m>%(extension)</m>
    </h>
  </ItemDefinitionGroup>
  <ItemGroup>
    <h Include='a.foo;a.bar'/>
  </ItemGroup>
  <Target Name='t'>
    <ItemGroup>
      <i Include=""@(h)"">
         <n Condition=""'%(m)'=='.foo'"">n1</n>
      </i>
    </ItemGroup>
  </Target>
</Project>");
 
            using ProjectFromString projectFromString = new(content);
            Project project = projectFromString.Project;
 
            ProjectInstance instance = project.CreateProjectInstance();
            MockLogger l = new MockLogger();
            List<ILogger> loggers = new List<ILogger>() { l };
            instance.Build(loggers);
 
            ProjectItemInstance item1 = instance.GetItems("i").ElementAt(0);
            Assert.Equal("n1", item1.GetMetadataValue("n"));
 
            ProjectItemInstance item2 = instance.GetItems("i").ElementAt(1);
            Assert.Equal("", item2.GetMetadataValue("n"));
        }
 
        /// <summary>
        /// Built-in metadata is prohibited in item definition conditions.
        /// Ideally it would also be late evaluated, but that's too difficult.
        /// </summary>
        [Fact]
        public void ExpandBuiltInMetadataAtPointOfUse_BuiltInProhibitedOnItemDefinitionMetadataCondition()
        {
            Assert.Throws<InvalidProjectFileException>(() =>
            {
                string content =
    ObjectModelHelpers.CleanupFileContents(
    @"<Project ToolsVersion=""msbuilddefaulttoolsversion"" xmlns=""msbuildnamespace"">
  <ItemDefinitionGroup>
    <i>
      <m Condition=""'%(filename)'!=''"">m1</m>
    </i>
  </ItemDefinitionGroup>
</Project>");
 
                Project project = new Project(XmlReader.Create(new StringReader(content)));
            });
        }
        /// <summary>
        /// Built-in metadata is prohibited in item definition conditions.
        /// Ideally it would also be late evaluated, but that's too difficult.
        /// </summary>
        [Fact]
        public void ExpandBuiltInMetadataAtPointOfUse_UnquotedBuiltInProhibitedOnItemDefinitionMetadataCondition()
        {
            Assert.Throws<InvalidProjectFileException>(() =>
            {
                string content =
    ObjectModelHelpers.CleanupFileContents(
    @"<Project ToolsVersion=""msbuilddefaulttoolsversion"" xmlns=""msbuildnamespace"">
  <ItemDefinitionGroup>
    <i>
      <m Condition=""%(filename)!=''"">m1</m>
    </i>
  </ItemDefinitionGroup>
</Project>");
 
                Project project = new Project(XmlReader.Create(new StringReader(content)));
            });
        }
        /// <summary>
        /// Built-in metadata is prohibited in item definition conditions.
        /// Ideally it would also be late evaluated, but that's too difficult.
        /// </summary>
        [Fact]
        public void ExpandBuiltInMetadataAtPointOfUse_BuiltInProhibitedOnItemDefinitionCondition()
        {
            Assert.Throws<InvalidProjectFileException>(() =>
            {
                string content =
    ObjectModelHelpers.CleanupFileContents(
    @"<Project ToolsVersion=""msbuilddefaulttoolsversion"" xmlns=""msbuildnamespace"">
  <ItemDefinitionGroup>
    <i Condition=""'%(filename)'!=''"">
      <m>m1</m>
    </i>
  </ItemDefinitionGroup>
</Project>");
 
                Project project = new Project(XmlReader.Create(new StringReader(content)));
            });
        }
        /// <summary>
        /// Built-in metadata is prohibited in item definition conditions.
        /// Ideally it would also be late evaluated, but that's too difficult.
        /// </summary>
        [Fact]
        public void ExpandBuiltInMetadataAtPointOfUse_BuiltInProhibitedOnItemDefinitionGroupCondition()
        {
            Assert.Throws<InvalidProjectFileException>(() =>
            {
                string content =
    ObjectModelHelpers.CleanupFileContents(
    @"<Project ToolsVersion=""msbuilddefaulttoolsversion"" xmlns=""msbuildnamespace"">
  <ItemDefinitionGroup Condition=""'%(filename)'!=''"">
    <i>
      <m>m1</m>
    </i>
  </ItemDefinitionGroup>
</Project>");
 
                Project project = new Project(XmlReader.Create(new StringReader(content)));
            });
        }
        /// <summary>
        /// Built-in metadata is prohibited in item definition conditions.
        /// Ideally it would also be late evaluated, but that's too difficult.
        /// </summary>
        [Fact]
        public void ExpandBuiltInMetadataAtPointOfUse_QualifiedBuiltInProhibitedOnItemDefinitionMetadataCondition()
        {
            Assert.Throws<InvalidProjectFileException>(() =>
            {
                string content =
    ObjectModelHelpers.CleanupFileContents(
    @"<Project ToolsVersion=""msbuilddefaulttoolsversion"" xmlns=""msbuildnamespace"">
  <ItemDefinitionGroup>
    <i>
      <m Condition=""'%(i.filename)'!=''"">m1</m>
    </i>
  </ItemDefinitionGroup>
</Project>");
 
                Project project = new Project(XmlReader.Create(new StringReader(content)));
            });
        }
        /// <summary>
        /// Built-in metadata is prohibited in item definition conditions.
        /// Ideally it would also be late evaluated, but that's too difficult.
        /// </summary>
        [Fact]
        public void ExpandBuiltInMetadataAtPointOfUse_QualifiedBuiltInProhibitedOnItemDefinitionCondition()
        {
            Assert.Throws<InvalidProjectFileException>(() =>
            {
                string content =
    ObjectModelHelpers.CleanupFileContents(
    @"<Project ToolsVersion=""msbuilddefaulttoolsversion"" xmlns=""msbuildnamespace"">
  <ItemDefinitionGroup>
    <i Condition=""'%(i.filename)'!=''"">
      <m>m1</m>
    </i>
  </ItemDefinitionGroup>
</Project>");
 
                Project project = new Project(XmlReader.Create(new StringReader(content)));
            });
        }
        /// <summary>
        /// Built-in metadata is prohibited in item definition conditions.
        /// Ideally it would also be late evaluated, but that's too difficult.
        /// </summary>
        [Fact]
        public void ExpandBuiltInMetadataAtPointOfUse_QualifiedBuiltInProhibitedOnItemDefinitionGroupCondition()
        {
            Assert.Throws<InvalidProjectFileException>(() =>
            {
                string content =
    ObjectModelHelpers.CleanupFileContents(
    @"<Project ToolsVersion=""msbuilddefaulttoolsversion"" xmlns=""msbuildnamespace"">
  <ItemDefinitionGroup Condition=""'%(i.filename)'!=''"">
    <i>
      <m>m1</m>
    </i>
  </ItemDefinitionGroup>
</Project>");
 
                Project project = new Project(XmlReader.Create(new StringReader(content)));
            });
        }
        /// <summary>
        /// Built-in metadata is prohibited in item definition conditions.
        /// Ideally it would also be late evaluated, but that's too difficult.
        /// </summary>
        [Fact]
        public void ExpandBuiltInMetadataAtPointOfUse_UnquotedQualifiedBuiltInProhibitedOnItemDefinitionCondition()
        {
            Assert.Throws<InvalidProjectFileException>(() =>
            {
                string content =
    ObjectModelHelpers.CleanupFileContents(
    @"<Project ToolsVersion=""msbuilddefaulttoolsversion"" xmlns=""msbuildnamespace"">
  <ItemDefinitionGroup>
    <i Condition=""%(i.filename)!=''"">
      <m>m1</m>
    </i>
  </ItemDefinitionGroup>
</Project>");
 
                Project project = new Project(XmlReader.Create(new StringReader(content)));
            });
        }
        /// <summary>
        /// Custom metadata is allowed in item definition conditions.
        /// </summary>
        [Fact]
        public void ExpandBuiltInMetadataAtPointOfUse_UnquotedQualifiedCustomAllowedOnItemDefinitionCondition()
        {
            string content =
ObjectModelHelpers.CleanupFileContents(
@"<Project ToolsVersion=""msbuilddefaulttoolsversion"" xmlns=""msbuildnamespace"">
  <ItemDefinitionGroup>
    <i Condition=""%(i.custom)!=''"">
      <m Condition=""%(i.custom)!=''"">m1</m>
    </i>
  </ItemDefinitionGroup>
</Project>");
 
            using ProjectFromString projectFromString = new(content);
            Project project = projectFromString.Project;  // No exception
        }
    }
}