File: Construction\ProjectFormatting_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.Text;
using System.Xml;
using Microsoft.Build.Construction;
using Microsoft.Build.Evaluation;
using Microsoft.Build.Shared;
using Microsoft.Build.UnitTests;
using Xunit;
using Xunit.Abstractions;
 
#nullable disable
 
namespace Microsoft.Build.Engine.OM.UnitTests.Construction
{
    public class ProjectFormatting_Tests : IDisposable
    {
        private readonly ITestOutputHelper _testOutput;
 
        public ProjectFormatting_Tests(ITestOutputHelper testOutput)
        {
            _testOutput = testOutput;
            Setup();
        }
 
        /// <summary>
        /// Clear out the cache
        /// </summary>
        private void Setup()
        {
            ProjectCollection.GlobalProjectCollection.UnloadAllProjects();
            GC.Collect();
        }
 
        /// <summary>
        /// Clear out the cache
        /// </summary>
        public void Dispose()
        {
            Setup();
        }
 
        [Fact]
        public void ProjectCommentFormatting()
        {
            string content = ObjectModelHelpers.CleanupFileContents(@"
<Project DefaultTargets=`Build` ToolsVersion=`msbuilddefaulttoolsversion` xmlns=`msbuildnamespace`>
  <ItemGroup>
    <ProjectReference Include=`..\CLREXE\CLREXE.vcxproj`><!-- Comment -->
      <Project>{3699f81b-2d03-46c5-abd7-e88a4c946f28}</Project>
    </ProjectReference>
  </ItemGroup>
</Project>");
 
            string reformattedContent = ObjectModelHelpers.CleanupFileContents(@"
<Project DefaultTargets=`Build` ToolsVersion=`msbuilddefaulttoolsversion` xmlns=`msbuildnamespace`>
  <ItemGroup>
    <ProjectReference Include=`..\CLREXE\CLREXE.vcxproj`>
      <!-- Comment -->
      <Project>{3699f81b-2d03-46c5-abd7-e88a4c946f28}</Project>
    </ProjectReference>
  </ItemGroup>
</Project>");
 
            VerifyFormattingPreserved(content);
            VerifyProjectReformatting(content, reformattedContent);
        }
 
        [Fact]
        public void ProjectWhitespaceFormatting()
        {
            // Note that there are two spaces after the <ItemGroup> tag on the second line
            string content = ObjectModelHelpers.CleanupFileContents(@"
<Project DefaultTargets=`Build` ToolsVersion=`msbuilddefaulttoolsversion` xmlns=`msbuildnamespace`>
  <ItemGroup>
    <ProjectReference Include=`..\CLREXE\CLREXE.vcxproj`>
<Project>{3699f81b-2d03-46c5-abd7-e88a4c946f28}</Project>
    </ProjectReference>
  </ItemGroup>
</Project>");
 
            string reformattedContent = ObjectModelHelpers.CleanupFileContents(@"
<Project DefaultTargets=`Build` ToolsVersion=`msbuilddefaulttoolsversion` xmlns=`msbuildnamespace`>
  <ItemGroup>
    <ProjectReference Include=`..\CLREXE\CLREXE.vcxproj`>
      <Project>{3699f81b-2d03-46c5-abd7-e88a4c946f28}</Project>
    </ProjectReference>
  </ItemGroup>
</Project>");
 
            VerifyFormattingPreserved(content);
            VerifyProjectReformatting(content, reformattedContent);
        }
 
        [Fact]
        public void ProjectAddItemFormatting_StartOfGroup()
        {
            string content = ObjectModelHelpers.CleanupFileContents(@"
<Project DefaultTargets=`Build` ToolsVersion=`msbuilddefaulttoolsversion` xmlns=`msbuildnamespace`>
  <ItemGroup>
    <Compile Include=""Class2.cs"" />
    <Compile Include=""Program.cs""/>
  </ItemGroup>
</Project>");
 
            using ProjectRootElementFromString projectRootElementFromString = new(
                content,
                ProjectCollection.GlobalProjectCollection,
                preserveFormatting: true);
            ProjectRootElement xml = projectRootElementFromString.Project;
 
            Project project = new Project(xml);
            project.AddItem("Compile", "Class1.cs");
            using StringWriter writer = new StringWriter();
            project.Save(writer);
 
            string expected = ObjectModelHelpers.CleanupFileContents(@"<?xml version=""1.0"" encoding=""utf-16""?>
<Project DefaultTargets=`Build` ToolsVersion=`msbuilddefaulttoolsversion` xmlns=`msbuildnamespace`>
  <ItemGroup>
    <Compile Include=""Class1.cs"" />
    <Compile Include=""Class2.cs"" />
    <Compile Include=""Program.cs"" />
  </ItemGroup>
</Project>");
 
            string actual = writer.ToString();
 
            VerifyAssertLineByLine(expected, actual);
        }
 
        [Fact]
        public void ProjectAddItemFormatting_MiddleOfGroup()
        {
            string content = ObjectModelHelpers.CleanupFileContents(@"
<Project DefaultTargets=`Build` ToolsVersion=`msbuilddefaulttoolsversion` xmlns=`msbuildnamespace`>
  <ItemGroup>
    <Compile Include=""Class1.cs"" />
    <Compile Include=""Program.cs""/>
  </ItemGroup>
</Project>");
 
            using ProjectRootElementFromString projectRootElementFromString = new(
                content,
                ProjectCollection.GlobalProjectCollection,
                preserveFormatting: true);
            ProjectRootElement xml = projectRootElementFromString.Project;
 
            Project project = new Project(xml);
            project.AddItem("Compile", "Class2.cs");
            using StringWriter writer = new StringWriter();
            project.Save(writer);
 
            string expected = ObjectModelHelpers.CleanupFileContents(@"<?xml version=""1.0"" encoding=""utf-16""?>
<Project DefaultTargets=`Build` ToolsVersion=`msbuilddefaulttoolsversion` xmlns=`msbuildnamespace`>
  <ItemGroup>
    <Compile Include=""Class1.cs"" />
    <Compile Include=""Class2.cs"" />
    <Compile Include=""Program.cs"" />
  </ItemGroup>
</Project>");
 
            string actual = writer.ToString();
 
            VerifyAssertLineByLine(expected, actual);
        }
 
        [Fact]
        public void ProjectAddItemFormatting_EndOfGroup()
        {
            string content = ObjectModelHelpers.CleanupFileContents(@"
<Project DefaultTargets=`Build` ToolsVersion=`msbuilddefaulttoolsversion` xmlns=`msbuildnamespace`>
  <ItemGroup>
    <Compile Include=""Class1.cs"" />
    <Compile Include=""Class2.cs""/>
  </ItemGroup>
</Project>");
 
            using ProjectRootElementFromString projectRootElementFromString = new(
                content,
                ProjectCollection.GlobalProjectCollection,
                preserveFormatting: true);
            ProjectRootElement xml = projectRootElementFromString.Project;
            Project project = new Project(xml);
            project.AddItem("Compile", "Program.cs");
            using StringWriter writer = new StringWriter();
            project.Save(writer);
 
            string expected = ObjectModelHelpers.CleanupFileContents(@"<?xml version=""1.0"" encoding=""utf-16""?>
<Project DefaultTargets=`Build` ToolsVersion=`msbuilddefaulttoolsversion` xmlns=`msbuildnamespace`>
  <ItemGroup>
    <Compile Include=""Class1.cs"" />
    <Compile Include=""Class2.cs"" />
    <Compile Include=""Program.cs"" />
  </ItemGroup>
</Project>");
 
            string actual = writer.ToString();
            VerifyAssertLineByLine(expected, actual);
        }
 
        [Fact]
        public void ProjectAddItemFormatting_EmptyGroup()
        {
            string content = ObjectModelHelpers.CleanupFileContents(@"<?xml version=""1.0"" encoding=""utf-8""?>
<Project DefaultTargets=`Build` ToolsVersion=`msbuilddefaulttoolsversion` xmlns=`msbuildnamespace`>
  <ItemGroup>
  </ItemGroup>
</Project>");
            using ProjectRootElementFromString projectRootElementFromString = new(
                content,
                ProjectCollection.GlobalProjectCollection,
                preserveFormatting: true);
            ProjectRootElement xml = projectRootElementFromString.Project;
 
            Project project = new Project(xml);
            project.AddItem("Compile", "Program.cs");
            using StringWriter writer = new EncodingStringWriter();
            project.Save(writer);
 
            string expected = ObjectModelHelpers.CleanupFileContents(@"<?xml version=""1.0"" encoding=""utf-8""?>
<Project DefaultTargets=`Build` ToolsVersion=`msbuilddefaulttoolsversion` xmlns=`msbuildnamespace`>
  <ItemGroup>
    <Compile Include=""Program.cs"" />
  </ItemGroup>
</Project>");
 
            string actual = writer.ToString();
 
            VerifyAssertLineByLine(expected, actual);
        }
 
        [Fact]
        public void ProjectAddItemFormatting_NoItemGroup()
        {
            string content = ObjectModelHelpers.CleanupFileContents(@"
<Project DefaultTargets=`Build` ToolsVersion=`msbuilddefaulttoolsversion` xmlns=`msbuildnamespace`>
</Project>");
 
            using ProjectRootElementFromString projectRootElementFromString = new(
                content,
                ProjectCollection.GlobalProjectCollection,
                preserveFormatting: true);
            ProjectRootElement xml = projectRootElementFromString.Project;
            Project project = new Project(xml);
            project.AddItem("Compile", "Program.cs");
            using StringWriter writer = new StringWriter();
            project.Save(writer);
 
            string expected = ObjectModelHelpers.CleanupFileContents(@"<?xml version=""1.0"" encoding=""utf-16""?>
<Project DefaultTargets=`Build` ToolsVersion=`msbuilddefaulttoolsversion` xmlns=`msbuildnamespace`>
  <ItemGroup>
    <Compile Include=""Program.cs"" />
  </ItemGroup>
</Project>");
 
            string actual = writer.ToString();
 
            VerifyAssertLineByLine(expected, actual);
        }
 
        [Fact]
        public void ProjectRemoveItemFormatting()
        {
            string content = ObjectModelHelpers.CleanupFileContents(@"
<Project DefaultTargets=`Build` ToolsVersion=`msbuilddefaulttoolsversion` xmlns=`msbuildnamespace`>
  <ItemGroup>
    <Compile Include=""Class1.cs"" />
    <Compile Include=""Class2.cs""/>
    <Compile Include=""Program.cs""/>
  </ItemGroup>
</Project>");
 
            using ProjectRootElementFromString projectRootElementFromString = new(
                content,
                ProjectCollection.GlobalProjectCollection,
                preserveFormatting: true);
            ProjectRootElement xml = projectRootElementFromString.Project;
 
            Project project = new Project(xml);
 
            var itemToRemove = project.GetItems("Compile").Single(item => item.EvaluatedInclude == "Class2.cs");
            project.RemoveItem(itemToRemove);
 
            using StringWriter writer = new StringWriter();
            project.Save(writer);
 
            string expected = ObjectModelHelpers.CleanupFileContents(@"<?xml version=""1.0"" encoding=""utf-16""?>
<Project DefaultTargets=`Build` ToolsVersion=`msbuilddefaulttoolsversion` xmlns=`msbuildnamespace`>
  <ItemGroup>
    <Compile Include=""Class1.cs"" />
    <Compile Include=""Program.cs"" />
  </ItemGroup>
</Project>");
 
            string actual = writer.ToString();
 
            VerifyAssertLineByLine(expected, actual);
        }
 
        [Fact]
        public void ProjectAddItemMetadataFormatting()
        {
            string content = ObjectModelHelpers.CleanupFileContents(@"
<Project DefaultTargets=`Build` ToolsVersion=`msbuilddefaulttoolsversion` xmlns=`msbuildnamespace`>
  <ItemGroup>
    <Compile Include=""Class1.cs"" />
    <Compile Include=""Class2.cs""/>
    <Compile Include=""Program.cs""/>
  </ItemGroup>
</Project>");
 
            using ProjectRootElementFromString projectRootElementFromString = new(
                content,
                ProjectCollection.GlobalProjectCollection,
                preserveFormatting: true);
            ProjectRootElement xml = projectRootElementFromString.Project;
 
            Project project = new Project(xml);
 
            var itemToEdit = project.GetItems("Compile").Single(item => item.EvaluatedInclude == "Class2.cs");
            itemToEdit.SetMetadataValue("ExcludeFromStyleCop", "true");
 
            using StringWriter writer = new StringWriter();
            project.Save(writer);
 
            string expected = ObjectModelHelpers.CleanupFileContents(@"<?xml version=""1.0"" encoding=""utf-16""?>
<Project DefaultTargets=`Build` ToolsVersion=`msbuilddefaulttoolsversion` xmlns=`msbuildnamespace`>
  <ItemGroup>
    <Compile Include=""Class1.cs"" />
    <Compile Include=""Class2.cs"">
      <ExcludeFromStyleCop>true</ExcludeFromStyleCop>
    </Compile>
    <Compile Include=""Program.cs"" />
  </ItemGroup>
</Project>");
 
            string actual = writer.ToString();
 
            VerifyAssertLineByLine(expected, actual);
        }
 
        [Fact(Skip = "https://github.com/dotnet/msbuild/issues/362")]
        public void PreprocessorFormatting()
        {
            string content = ObjectModelHelpers.CleanupFileContents(@"
<Project DefaultTargets='Build' ToolsVersion='msbuilddefaulttoolsversion' xmlns='msbuildnamespace'>
  <Target
    Name=""XamlPreCompile""
    Inputs=""$(MSBuildAllProjects);
           @(Compile);
           @(_CoreCompileResourceInputs);""
  />
</Project>");
 
            using ProjectRootElementFromString projectRootElementFromString = new(
                content,
                ProjectCollection.GlobalProjectCollection,
                preserveFormatting: true);
            ProjectRootElement xml = projectRootElementFromString.Project;
 
            Project project = new Project(xml);
 
            using StringWriter writer = new StringWriter();
 
            project.SaveLogicalProject(writer);
 
            string actual = writer.ToString();
            string expected = @"<?xml version=""1.0"" encoding=""utf-16""?>" +
                content;
 
            VerifyAssertLineByLine(expected, actual);
        }
 
        private void VerifyFormattingPreserved(string projectContents)
        {
            VerifyFormattingPreservedFromString(projectContents);
            VerifyFormattingPreservedFromFile(projectContents);
        }
 
        private void VerifyFormattingPreservedFromString(string projectContents)
        {
            using ProjectRootElementFromString projectRootElementFromString = new(
                projectContents,
                ProjectCollection.GlobalProjectCollection,
                preserveFormatting: true);
            ProjectRootElement xml = projectRootElementFromString.Project;
 
            Project project = new Project(xml);
            using StringWriter writer = new StringWriter();
            project.Save(writer);
 
            string expected = @"<?xml version=""1.0"" encoding=""utf-16""?>" +
                projectContents;
            string actual = writer.ToString();
 
            VerifyAssertLineByLine(expected, actual);
        }
 
        private void VerifyFormattingPreservedFromFile(string projectContents)
        {
            string directory = null;
 
            try
            {
                directory = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString());
                Directory.CreateDirectory(directory);
 
                string file = Path.Combine(directory, "test.proj");
                File.WriteAllText(file, projectContents);
 
                ProjectRootElement xml = ProjectRootElement.Open(file, ProjectCollection.GlobalProjectCollection,
                    preserveFormatting: true);
                Project project = new Project(xml);
                using StringWriter writer = new StringWriter();
                project.Save(writer);
 
                string expected = @"<?xml version=""1.0"" encoding=""utf-16""?>" +
                    projectContents;
                string actual = writer.ToString();
 
                VerifyAssertLineByLine(expected, actual);
            }
            finally
            {
                Directory.Delete(directory, true);
            }
        }
 
        private void VerifyProjectReformatting(string originalContents, string expectedContents)
        {
            VerifyProjectReformattingFromString(originalContents, expectedContents);
            VerifyProjectReformattingFromFile(originalContents, expectedContents);
        }
 
        private void VerifyProjectReformattingFromString(string originalContents, string expectedContents)
        {
            using ProjectRootElementFromString projectRootElementFromString = new(
                originalContents,
                ProjectCollection.GlobalProjectCollection,
                preserveFormatting: false);
            ProjectRootElement xml = projectRootElementFromString.Project;
 
            Project project = new Project(xml);
            using StringWriter writer = new StringWriter();
            project.Save(writer);
 
            string expected = @"<?xml version=""1.0"" encoding=""utf-16""?>" +
                expectedContents;
            string actual = writer.ToString();
 
            VerifyAssertLineByLine(expected, actual);
        }
 
        private void VerifyProjectReformattingFromFile(string originalContents, string expectedContents)
        {
            string directory = null;
 
            try
            {
                directory = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString());
                Directory.CreateDirectory(directory);
 
                string file = Path.Combine(directory, "test.proj");
                File.WriteAllText(file, originalContents);
 
                ProjectRootElement xml = ProjectRootElement.Open(file, ProjectCollection.GlobalProjectCollection,
                    preserveFormatting: false);
                Project project = new Project(xml);
                using StringWriter writer = new StringWriter();
                project.Save(writer);
 
                string expected = @"<?xml version=""1.0"" encoding=""utf-16""?>" +
                    expectedContents;
                string actual = writer.ToString();
 
                VerifyAssertLineByLine(expected, actual);
            }
            finally
            {
                Directory.Delete(directory, true);
            }
        }
 
        private void VerifyAssertLineByLine(string expected, string actual)
        {
            Helpers.VerifyAssertLineByLine(expected, actual, false, _testOutput);
        }
 
        [Fact]
        public void VerifyNamespaceRemainsWhenPresent()
        {
            string content = ObjectModelHelpers.CleanupFileContents(@"
<Project xmlns=`msbuildnamespace`>
  <ItemGroup>
    <ProjectReference Include=`..\CLREXE\CLREXE.vcxproj`>
      <Project>{3699f81b-2d03-46c5-abd7-e88a4c946f28}</Project>
    </ProjectReference>
  </ItemGroup>
</Project>");
 
            VerifyFormattingPreserved(content);
        }
 
        [Fact]
        public void VerifyNoNamespaceRemains()
        {
            string content = ObjectModelHelpers.CleanupFileContents(@"
<Project>
  <ItemGroup>
    <ProjectReference Include=`..\CLREXE\CLREXE.vcxproj` />
  </ItemGroup>
</Project>");
 
            VerifyFormattingPreserved(content);
        }
 
        [Fact]
        public void DefaultProjectSaveContainsAllNewFileOptions()
        {
            // XML declaration tag, namespace, and tools version must be present by default.
            string expected = ObjectModelHelpers.CleanupFileContents(@"<?xml version=""1.0"" encoding=""utf-8""?>
<Project ToolsVersion=""msbuilddefaulttoolsversion"" xmlns=""msbuildnamespace"">
  <ItemGroup>
    <ProjectReference Include=`..\CLREXE\CLREXE.vcxproj`>
      <metadata>value</metadata>
    </ProjectReference>
  </ItemGroup>
</Project>");
 
            Project project = new Project();
            project.AddItem("ProjectReference", @"..\CLREXE\CLREXE.vcxproj",
                new[] { new KeyValuePair<string, string>("metadata", "value") });
 
            using StringWriter writer = new EncodingStringWriter();
            project.Save(writer);
 
            string actual = writer.ToString();
 
            VerifyAssertLineByLine(expected, actual);
        }
 
        [Fact]
        public void NewProjectSaveWithOptionsNone()
        {
            // When NewProjectFileOptions.None is specified, we should not have an XML declaration,
            // tools version, or namespace in the project file.
            string expected = ObjectModelHelpers.CleanupFileContents(@"<Project>
  <ItemGroup>
    <ProjectReference Include=`..\CLREXE\CLREXE.vcxproj`>
      <metadata>value</metadata>
    </ProjectReference>
  </ItemGroup>
</Project>");
 
            Project project = new Project(NewProjectFileOptions.None);
            var item = project.AddItem("ProjectReference", @"..\CLREXE\CLREXE.vcxproj");
            item[0].SetMetadataValue("metadata", "value");
 
            using StringWriter writer = new EncodingStringWriter();
            project.Save(writer);
 
            string actual = writer.ToString();
 
            VerifyAssertLineByLine(expected, actual);
        }
 
        [Fact]
        public void ChangeItemTypeNoNamespace()
        {
            string expected = ObjectModelHelpers.CleanupFileContents(@"<Project>
  <ItemGroup>
    <ProjectReference Include=`..\CLREXE\CLREXE.vcxproj` />
  </ItemGroup>
</Project>");
 
            Project project = new Project(NewProjectFileOptions.None);
            var item = project.AddItem("NotProjectReference", @"..\CLREXE\CLREXE.vcxproj");
            item[0].ItemType = "ProjectReference";
 
            using StringWriter writer = new EncodingStringWriter();
            project.Save(writer);
 
            string actual = writer.ToString();
 
            VerifyAssertLineByLine(expected, actual);
        }
 
        [Fact]
        public void ChangeItemTypeWithNamespace()
        {
            string expected = ObjectModelHelpers.CleanupFileContents(@"<?xml version=`1.0` encoding=`utf-16`?>
<Project xmlns=`msbuildnamespace`>
  <ItemGroup>
    <ProjectReference Include=`..\CLREXE\CLREXE.vcxproj` />
  </ItemGroup>
</Project>");
 
            Project project = new Project(NewProjectFileOptions.IncludeXmlNamespace);
            var item = project.AddItem("NotProjectReference", @"..\CLREXE\CLREXE.vcxproj");
            item[0].ItemType = "ProjectReference";
 
            // StringWriter is UTF16 (will output xml declaration)
            using StringWriter writer = new StringWriter();
            project.Save(writer);
 
            string actual = writer.ToString();
 
            VerifyAssertLineByLine(expected, actual);
        }
 
        [Fact]
        public void ChangeItemTypeWithXmlHeader()
        {
            string expected = ObjectModelHelpers.CleanupFileContents(@"<?xml version=""1.0"" encoding=""utf-8""?>
<Project>
  <ItemGroup>
    <ProjectReference Include=`..\CLREXE\CLREXE.vcxproj` />
  </ItemGroup>
</Project>");
 
            Project project = new Project(NewProjectFileOptions.IncludeXmlDeclaration);
            var item = project.AddItem("NotProjectReference", @"..\CLREXE\CLREXE.vcxproj");
            item[0].ItemType = "ProjectReference";
 
            // Should still output XML declaration even when using UTF8 (NewProjectFileOptions.IncludeXmlDeclaration
            // was specified)
            using StringWriter writer = new EncodingStringWriter();
            project.Save(writer);
 
            string actual = writer.ToString();
 
            VerifyAssertLineByLine(expected, actual);
        }
 
        [Fact]
        public void VerifyUtf8WithoutBomTreatedAsUtf8()
        {
            string expected = ObjectModelHelpers.CleanupFileContents(@"<Project>
</Project>");
 
            Project project = new Project(NewProjectFileOptions.None);
            using StringWriter writer = new EncodingStringWriter(new UTF8Encoding(encoderShouldEmitUTF8Identifier: true));
            project.Save(writer);
 
            string actual = writer.ToString();
 
            VerifyAssertLineByLine(expected, actual);
        }
 
        [Fact]
        public void ProjectFileWithBomContainsBomAfterSave()
        {
            CreateProjectAndAssertEncoding(xmlDeclaration: false, byteOrderMark: true);
        }
 
        [Fact]
        public void ProjectFileWithoutBomDoesNotContainsBomAfterSave()
        {
            CreateProjectAndAssertEncoding(xmlDeclaration: false, byteOrderMark: false);
        }
 
        [Fact]
        public void ProjectFileWithoutBomWithXmlDeclarationDoesNotContainsBomAfterSave()
        {
            CreateProjectAndAssertEncoding(xmlDeclaration: true, byteOrderMark: false);
        }
 
        [Fact]
        public void ProjectFileWithBomWithXmlDeclarationContainsBomAfterSave()
        {
            CreateProjectAndAssertEncoding(xmlDeclaration: true, byteOrderMark: true);
        }
 
        private static void CreateProjectAndAssertEncoding(bool xmlDeclaration, bool byteOrderMark)
        {
            string declaration = @"<?xml version=""1.0"" encoding=""utf-8""?>";
 
            string content = xmlDeclaration ? declaration : string.Empty;
            content += @"<Project><Target Name=""Build""/></Project>";
            content = ObjectModelHelpers.CleanupFileContents(content);
 
            var file = FileUtilities.GetTemporaryFileName(".proj");
            try
            {
                File.WriteAllText(file, content, new UTF8Encoding(encoderShouldEmitUTF8Identifier: byteOrderMark));
                Assert.Equal(byteOrderMark, EncodingUtilities.FileStartsWithPreamble(file));
 
                // Load and manipulate/save the project
                var project = new Project(ProjectRootElement.Open(file, ProjectCollection.GlobalProjectCollection));
                project.AddItem("Compile", "Program.cs");
                project.Save();
 
                // Ensure the file was really saved and that the presence of a BOM has not changed
                string actualContents = File.ReadAllText(file);
                Assert.Contains("<Compile Include=\"Program.cs\" />", actualContents);
                if (xmlDeclaration)
                {
                    Assert.Contains(declaration, actualContents);
                }
                else
                {
                    Assert.DoesNotContain(declaration, actualContents);
                }
                Assert.Equal(byteOrderMark, EncodingUtilities.FileStartsWithPreamble(file));
            }
            finally
            {
                FileUtilities.DeleteDirectoryNoThrow(Path.GetDirectoryName(file), false);
            }
        }
    }
}