diff --git a/src/Shared/UnitTests/ObjectModelHelpers.cs b/src/Shared/UnitTests/ObjectModelHelpers.cs index ed3320b184e..68baea39c49 100644 --- a/src/Shared/UnitTests/ObjectModelHelpers.cs +++ b/src/Shared/UnitTests/ObjectModelHelpers.cs @@ -18,6 +18,7 @@ using Microsoft.Build.Logging; using Microsoft.Build.Shared; using Xunit; +using Xunit.Abstractions; namespace Microsoft.Build.UnitTests { @@ -1318,8 +1319,10 @@ internal static void VerifyAssertThrowsInvalidOperation(Action method) /// /// Verify that the expected content matches the actual content /// - private static void VerifyAssertLineByLine(string expected, string actual, bool ignoreFirstLineOfActual) + internal static void VerifyAssertLineByLine(string expected, string actual, bool ignoreFirstLineOfActual, ITestOutputHelper testOutput = null) { + Action LogLine = testOutput == null ? (Action) Console.WriteLine : testOutput.WriteLine; + string[] actualLines = SplitIntoLines(actual); if (ignoreFirstLineOfActual) @@ -1344,7 +1347,7 @@ private static void VerifyAssertLineByLine(string expected, string actual, bool if (expectedLines[i] != actualLines[i]) { expectedAndActualDontMatch = true; - Console.WriteLine("< " + expectedLines[i] + "\n> " + actualLines[i] + "\n"); + LogLine("< " + expectedLines[i] + "\n> " + actualLines[i] + "\n"); } } @@ -1358,15 +1361,15 @@ private static void VerifyAssertLineByLine(string expected, string actual, bool if (actualLines.Length > expectedLines.Length) { - Console.WriteLine("\n#################################Expected#################################\n" + String.Join("\n", expectedLines)); - Console.WriteLine("#################################Actual#################################\n" + String.Join("\n", actualLines)); + LogLine("\n#################################Expected#################################\n" + String.Join("\n", expectedLines)); + LogLine("#################################Actual#################################\n" + String.Join("\n", actualLines)); Assert.True(false, "Expected content was shorter, actual had this extra line: '" + actualLines[expectedLines.Length] + "'"); } else if (actualLines.Length < expectedLines.Length) { - Console.WriteLine("\n#################################Expected#################################\n" + String.Join("\n", expectedLines)); - Console.WriteLine("#################################Actual#################################\n" + String.Join("\n", actualLines)); + LogLine("\n#################################Expected#################################\n" + String.Join("\n", expectedLines)); + LogLine("#################################Actual#################################\n" + String.Join("\n", actualLines)); Assert.True(false, "Actual content was shorter, expected had this extra line: '" + expectedLines[actualLines.Length] + "'"); } diff --git a/src/XMakeBuildEngine/Construction/ProjectElementContainer.cs b/src/XMakeBuildEngine/Construction/ProjectElementContainer.cs index 0ff7fa85041..a3892ae1cb6 100644 --- a/src/XMakeBuildEngine/Construction/ProjectElementContainer.cs +++ b/src/XMakeBuildEngine/Construction/ProjectElementContainer.cs @@ -23,6 +23,8 @@ namespace Microsoft.Build.Construction /// public abstract class ProjectElementContainer : ProjectElement { + const string DEFAULT_INDENT = " "; + /// /// Number of children of any kind /// @@ -170,6 +172,18 @@ public void InsertAfterChild(ProjectElement child, ProjectElement reference) } XmlElement.InsertAfter(child.XmlElement, reference.XmlElement); + if (XmlDocument.PreserveWhitespace) + { + // If we are trying to preserve formatting of the file, then the new node won't automatically be indented. + // So try to match the surrounding formatting by checking the whitespace that precedes the node we inserted + // after, and inserting the same whitespace between the previous node and the one we added + if (reference.XmlElement.PreviousSibling != null && + reference.XmlElement.PreviousSibling.NodeType == XmlNodeType.Whitespace) + { + var newWhitespaceNode = XmlDocument.CreateWhitespace(reference.XmlElement.PreviousSibling.Value); + XmlElement.InsertAfter(newWhitespaceNode, reference.XmlElement); + } + } _count++; MarkDirty("Insert element {0}", child.ElementName); @@ -221,6 +235,19 @@ public void InsertBeforeChild(ProjectElement child, ProjectElement reference) XmlElement.InsertBefore(child.XmlElement, reference.XmlElement); + if (XmlDocument.PreserveWhitespace) + { + // If we are trying to preserve formatting of the file, then the new node won't automatically be indented. + // So try to match the surrounding formatting by by checking the whitespace that precedes where we inserted + // the new node, and inserting the same whitespace between the node we added and the one after it. + if (child.XmlElement.PreviousSibling != null && + child.XmlElement.PreviousSibling.NodeType == XmlNodeType.Whitespace) + { + var newWhitespaceNode = XmlDocument.CreateWhitespace(child.XmlElement.PreviousSibling.Value); + XmlElement.InsertBefore(newWhitespaceNode, reference.XmlElement); + } + } + _count++; MarkDirty("Insert element {0}", child.ElementName); } @@ -305,8 +332,20 @@ public void RemoveChild(ProjectElement child) LastChild = child.PreviousSibling; } + var previousSibling = child.XmlElement.PreviousSibling; + XmlElement.RemoveChild(child.XmlElement); + if (XmlDocument.PreserveWhitespace) + { + // If we are trying to preserve formatting of the file, then also remove any whitespace + // that came before the node we removed. + if (previousSibling != null && previousSibling.NodeType == XmlNodeType.Whitespace) + { + XmlElement.RemoveChild(previousSibling); + } + } + _count--; MarkDirty("Remove element {0}", child.ElementName); } @@ -437,6 +476,38 @@ private void AddInitialChild(ProjectElement child) XmlElement.AppendChild(child.XmlElement); + if (XmlDocument.PreserveWhitespace) + { + // If we are trying to preserve formatting of the file, then the new node won't automatically be indented. + // So try to match the surrounding formatting and add one indentation level + if (XmlElement.FirstChild.NodeType == XmlNodeType.Whitespace) + { + // This container had a whitespace node, which should generally be a newline and the indent + // before the closing tag. So we add the default indentation to it so the child will now be indented + // further, and then create a new whitespace node after the child so the closing tag will be on + // a new line with the same indentation. + // If the whitespace we end up copying isn't actually (newline + indentation) like we expect, then it + // should still be OK to copy it, as we'll still be trying to match the surrounding formatting. + string whitespace = XmlElement.FirstChild.Value; + XmlElement.FirstChild.Value = whitespace + DEFAULT_INDENT; + var newWhitespaceNode = XmlDocument.CreateWhitespace(whitespace); + XmlElement.InsertAfter(newWhitespaceNode, child.XmlElement); + } + else if (XmlElement.PreviousSibling != null && + XmlElement.PreviousSibling.NodeType == XmlNodeType.Whitespace) + { + // This container didn't have any whitespace in it. This probably means it didn't have separate open + // and close tags. So add a whitespace node before the new child with additional indentation over the + // container's indentation, and add a whitespace node with the same level of indentation as the container + // after the new child so the closing tag will be indented properly. + string parentWhitespace = XmlElement.PreviousSibling.Value; + var indentedWhitespaceNode = XmlDocument.CreateWhitespace(parentWhitespace + DEFAULT_INDENT); + XmlElement.InsertBefore(indentedWhitespaceNode, child.XmlElement); + var unindentedWhitespaceNode = XmlDocument.CreateWhitespace(parentWhitespace); + XmlElement.InsertAfter(unindentedWhitespaceNode, child.XmlElement); + } + } + _count++; MarkDirty("Add child element named '{0}'", child.ElementName); } diff --git a/src/XMakeBuildEngine/Construction/ProjectRootElement.cs b/src/XMakeBuildEngine/Construction/ProjectRootElement.cs index 93e9ce9174f..9ff53b33e4a 100644 --- a/src/XMakeBuildEngine/Construction/ProjectRootElement.cs +++ b/src/XMakeBuildEngine/Construction/ProjectRootElement.cs @@ -58,6 +58,8 @@ public class ProjectRootElement : ProjectElementContainer /// private static readonly ProjectRootElementCache.OpenProjectRootElement s_openLoaderDelegate = OpenLoader; + private static readonly ProjectRootElementCache.OpenProjectRootElement s_openLoaderPreserveFormattingDelegate = OpenLoaderPreserveFormatting; + /// /// The default encoding to use / assume for a new project. /// @@ -154,7 +156,8 @@ public class ProjectRootElement : ProjectElementContainer /// Leaves the project dirty, indicating there are unsaved changes. /// Used to create a root element for solutions loaded by the 3.5 version of the solution wrapper. /// - internal ProjectRootElement(XmlReader xmlReader, ProjectRootElementCache projectRootElementCache, bool isExplicitlyLoaded) + internal ProjectRootElement(XmlReader xmlReader, ProjectRootElementCache projectRootElementCache, bool isExplicitlyLoaded, + bool preserveFormatting) : base() { ErrorUtilities.VerifyThrowArgumentNull(xmlReader, "xmlReader"); @@ -165,7 +168,7 @@ internal ProjectRootElement(XmlReader xmlReader, ProjectRootElementCache project _directory = NativeMethodsShared.GetCurrentDirectory(); IncrementVersion(); - XmlDocumentWithLocation document = LoadDocument(xmlReader); + XmlDocumentWithLocation document = LoadDocument(xmlReader, preserveFormatting); ProjectParser.Parse(document, this); } @@ -200,7 +203,8 @@ private ProjectRootElement(ProjectRootElementCache projectRootElementCache) /// Assumes path is already normalized. /// May throw InvalidProjectFileException. /// - private ProjectRootElement(string path, ProjectRootElementCache projectRootElementCache, BuildEventContext buildEventContext) + private ProjectRootElement(string path, ProjectRootElementCache projectRootElementCache, BuildEventContext buildEventContext, + bool preserveFormatting) : base() { ErrorUtilities.VerifyThrowArgumentLength(path, "path"); @@ -214,7 +218,7 @@ private ProjectRootElement(string path, ProjectRootElementCache projectRootEleme _versionOnDisk = _version; _timeLastChangedUtc = DateTime.UtcNow; - XmlDocumentWithLocation document = LoadDocument(path); + XmlDocumentWithLocation document = LoadDocument(path, preserveFormatting); ProjectParser.Parse(document, this); @@ -964,7 +968,7 @@ public static ProjectRootElement Create(string path, ProjectCollection projectCo /// public static ProjectRootElement Create(XmlReader xmlReader) { - return Create(xmlReader, ProjectCollection.GlobalProjectCollection); + return Create(xmlReader, ProjectCollection.GlobalProjectCollection, preserveFormatting: false); } /// @@ -972,11 +976,12 @@ public static ProjectRootElement Create(XmlReader xmlReader) /// Uses the specified project collection. /// May throw InvalidProjectFileException. /// - public static ProjectRootElement Create(XmlReader xmlReader, ProjectCollection projectCollection) + public static ProjectRootElement Create(XmlReader xmlReader, ProjectCollection projectCollection, bool preserveFormatting) { ErrorUtilities.VerifyThrowArgumentNull(projectCollection, "projectCollection"); - return new ProjectRootElement(xmlReader, projectCollection.ProjectRootElementCache, true /*Explicitly loaded*/); + return new ProjectRootElement(xmlReader, projectCollection.ProjectRootElementCache, true /*Explicitly loaded*/, + preserveFormatting); } /// @@ -995,13 +1000,19 @@ public static ProjectRootElement Open(string path) /// May throw InvalidProjectFileException. /// public static ProjectRootElement Open(string path, ProjectCollection projectCollection) + { + return Open(path, projectCollection, + preserveFormatting: false); + } + + public static ProjectRootElement Open(string path, ProjectCollection projectCollection, bool preserveFormatting) { ErrorUtilities.VerifyThrowArgumentLength(path, "path"); ErrorUtilities.VerifyThrowArgumentNull(projectCollection, "projectCollection"); path = FileUtilities.NormalizePath(path); - return Open(path, projectCollection.ProjectRootElementCache, true /*Is explicitly loaded*/); + return Open(path, projectCollection.ProjectRootElementCache, true /*Is explicitly loaded*/, preserveFormatting); } /// @@ -1735,11 +1746,14 @@ internal static ProjectRootElement Create(ProjectRootElementCache projectRootEle /// Uses the specified project root element cache. /// May throw InvalidProjectFileException. /// - internal static ProjectRootElement Open(string path, ProjectRootElementCache projectRootElementCache, bool isExplicitlyLoaded) + internal static ProjectRootElement Open(string path, ProjectRootElementCache projectRootElementCache, bool isExplicitlyLoaded, + bool preserveFormatting) { ErrorUtilities.VerifyThrowInternalRooted(path); - ProjectRootElement projectRootElement = projectRootElementCache.Get(path, s_openLoaderDelegate, isExplicitlyLoaded); + ProjectRootElement projectRootElement = projectRootElementCache.Get(path, + preserveFormatting ? s_openLoaderPreserveFormattingDelegate : s_openLoaderDelegate, + isExplicitlyLoaded, preserveFormatting); return projectRootElement; } @@ -1771,8 +1785,10 @@ internal static ProjectRootElement OpenProjectOrSolution(string fullPath, IDicti ProjectRootElement projectRootElement = projectRootElementCache.Get( fullPath, - (path, cache) => CreateProjectFromPath(path, globalProperties, toolsVersion, loggingService, cache, buildEventContext), - isExplicitlyLoaded); + (path, cache) => CreateProjectFromPath(path, globalProperties, toolsVersion, loggingService, cache, buildEventContext, + preserveFormatting: false), + isExplicitlyLoaded, + preserveFormatting: false); return projectRootElement; } @@ -1869,11 +1885,24 @@ protected override ProjectElement CreateNewInstance(ProjectRootElement owner) /// The path to the file to load. /// The cache to load the PRE into. private static ProjectRootElement OpenLoader(string path, ProjectRootElementCache projectRootElementCache) + { + return OpenLoader(path, projectRootElementCache, + preserveFormatting: false); + } + + private static ProjectRootElement OpenLoaderPreserveFormatting(string path, ProjectRootElementCache projectRootElementCache) + { + return OpenLoader(path, projectRootElementCache, + preserveFormatting: true); + } + + private static ProjectRootElement OpenLoader(string path, ProjectRootElementCache projectRootElementCache, bool preserveFormatting) { return new ProjectRootElement( path, projectRootElementCache, - new BuildEventContext(0, BuildEventContext.InvalidNodeId, BuildEventContext.InvalidProjectContextId, BuildEventContext.InvalidTaskId)); + new BuildEventContext(0, BuildEventContext.InvalidNodeId, BuildEventContext.InvalidProjectContextId, BuildEventContext.InvalidTaskId), + preserveFormatting); } /// @@ -1890,7 +1919,8 @@ private static ProjectRootElement CreateProjectFromPath string toolsVersion, ILoggingService loggingService, ProjectRootElementCache projectRootElementCache, - BuildEventContext buildEventContext + BuildEventContext buildEventContext, + bool preserveFormatting ) { ErrorUtilities.VerifyThrowInternalRooted(projectFile); @@ -1903,7 +1933,7 @@ BuildEventContext buildEventContext } // OK it's a regular project file, load it normally. - return new ProjectRootElement(projectFile, projectRootElementCache, buildEventContext); + return new ProjectRootElement(projectFile, projectRootElementCache, buildEventContext, preserveFormatting); } catch (InvalidProjectFileException) { @@ -1924,11 +1954,12 @@ BuildEventContext buildEventContext /// Does NOT add to the ProjectRootElementCache. Caller should add after verifying subsequent MSBuild parsing succeeds. /// /// The full path to the document to load. - private XmlDocumentWithLocation LoadDocument(string fullPath) + private XmlDocumentWithLocation LoadDocument(string fullPath, bool preserveFormatting) { ErrorUtilities.VerifyThrowInternalRooted(fullPath); XmlDocumentWithLocation document = new XmlDocumentWithLocation(); + document.PreserveWhitespace = preserveFormatting; #if (!STANDALONEBUILD) using (new CodeMarkerStartEnd(CodeMarkerEvent.perfMSBuildProjectLoadFromFileBegin, CodeMarkerEvent.perfMSBuildProjectLoadFromFileEnd)) #endif @@ -1998,9 +2029,10 @@ private XmlDocumentWithLocation LoadDocument(string fullPath) /// May throw InvalidProjectFileException. /// Never returns null. /// - private XmlDocumentWithLocation LoadDocument(XmlReader reader) + private XmlDocumentWithLocation LoadDocument(XmlReader reader, bool preserveFormatting) { XmlDocumentWithLocation document = new XmlDocumentWithLocation(); + document.PreserveWhitespace = preserveFormatting; try { diff --git a/src/XMakeBuildEngine/Definition/Project.cs b/src/XMakeBuildEngine/Definition/Project.cs index 51f3fef3720..25c3e2b5f57 100644 --- a/src/XMakeBuildEngine/Definition/Project.cs +++ b/src/XMakeBuildEngine/Definition/Project.cs @@ -359,7 +359,8 @@ public Project(XmlReader xmlReader, IDictionary globalProperties try { - _xml = ProjectRootElement.Create(xmlReader, projectCollection); + _xml = ProjectRootElement.Create(xmlReader, projectCollection, + preserveFormatting: false); } catch (InvalidProjectFileException ex) { diff --git a/src/XMakeBuildEngine/Definition/Toolset.cs b/src/XMakeBuildEngine/Definition/Toolset.cs index 937e5764e0a..867e15d0cfd 100644 --- a/src/XMakeBuildEngine/Definition/Toolset.cs +++ b/src/XMakeBuildEngine/Definition/Toolset.cs @@ -1011,7 +1011,9 @@ private void LoadAndRegisterFromTasksFile(string searchPath, string[] defaultTas } else { - projectRootElement = ProjectRootElement.Open(defaultTasksFile, projectRootElementCache, false /*The tasks file is not a explicitly loaded file*/); + projectRootElement = ProjectRootElement.Open(defaultTasksFile, projectRootElementCache, + false /*The tasks file is not a explicitly loaded file*/, + preserveFormatting: false); } foreach (ProjectElement elementXml in projectRootElement.Children) diff --git a/src/XMakeBuildEngine/Evaluation/Evaluator.cs b/src/XMakeBuildEngine/Evaluation/Evaluator.cs index 40543448b2b..03198e67c5a 100644 --- a/src/XMakeBuildEngine/Evaluation/Evaluator.cs +++ b/src/XMakeBuildEngine/Evaluation/Evaluator.cs @@ -2424,7 +2424,8 @@ private LoadImportsResult ExpandAndLoadImportsFromUnescapedImportExpression(stri _projectRootElementCache, _buildEventContext, explicitlyLoaded), - explicitlyLoaded); + explicitlyLoaded, + preserveFormatting: false); if (duplicateImport) { diff --git a/src/XMakeBuildEngine/Evaluation/ProjectRootElementCache.cs b/src/XMakeBuildEngine/Evaluation/ProjectRootElementCache.cs index ae6e8b4ccaa..6f9f8ae54ed 100644 --- a/src/XMakeBuildEngine/Evaluation/ProjectRootElementCache.cs +++ b/src/XMakeBuildEngine/Evaluation/ProjectRootElementCache.cs @@ -191,7 +191,8 @@ internal ProjectRootElementCache(bool autoReloadFromDisk) /// The project file which contains the ProjectRootElement. Must be a full path. /// The delegate to use to load if necessary. May be null. /// The ProjectRootElement instance if one exists. Null otherwise. - internal ProjectRootElement Get(string projectFile, OpenProjectRootElement openProjectRootElement, bool isExplicitlyLoaded) + internal ProjectRootElement Get(string projectFile, OpenProjectRootElement openProjectRootElement, bool isExplicitlyLoaded, + bool preserveFormatting) { // Should already have been canonicalized ErrorUtilities.VerifyThrowInternalRooted(projectFile); @@ -201,6 +202,12 @@ internal ProjectRootElement Get(string projectFile, OpenProjectRootElement openP ProjectRootElement projectRootElement; _weakCache.TryGetValue(projectFile, out projectRootElement); + if (projectRootElement != null && projectRootElement.XmlDocument.PreserveWhitespace != preserveFormatting) + { + // Cached project doesn't match preserveFormatting setting, so don't use it + projectRootElement = null; + } + if (projectRootElement != null && _autoReloadFromDisk) { FileInfo fileInfo = FileUtilities.GetFileInfoNoThrow(projectFile); @@ -227,6 +234,7 @@ internal ProjectRootElement Get(string projectFile, OpenProjectRootElement openP // use: it checks the file content as well as the timestamp. That's better than completely disabling // the cache as we get test coverage of the rest of the cache code. XmlDocument document = new XmlDocument(); + document.PreserveWhitespace = projectRootElement.XmlDocument.PreserveWhitespace; using (XmlTextReader xtr = new XmlTextReader(projectRootElement.FullPath)) { xtr.DtdProcessing = DtdProcessing.Ignore; @@ -336,7 +344,8 @@ internal void RenameEntry(string oldFullPath, ProjectRootElement projectRootElem /// internal ProjectRootElement TryGet(string projectFile) { - ProjectRootElement result = Get(projectFile, null /* no delegate to load it */, false /*Since we are not creating a PRE this can be true or false*/); + ProjectRootElement result = Get(projectFile, null /* no delegate to load it */, false, /*Since we are not creating a PRE this can be true or false*/ + preserveFormatting: false); return result; } diff --git a/src/XMakeBuildEngine/Instance/ProjectInstance.cs b/src/XMakeBuildEngine/Instance/ProjectInstance.cs index c89ae06c182..0c8bf9f3777 100644 --- a/src/XMakeBuildEngine/Instance/ProjectInstance.cs +++ b/src/XMakeBuildEngine/Instance/ProjectInstance.cs @@ -2127,7 +2127,8 @@ bool isExplicitlyLoaded XmlReaderSettings xrs = new XmlReaderSettings(); xrs.DtdProcessing = DtdProcessing.Ignore; - ProjectRootElement projectRootElement = new ProjectRootElement(XmlReader.Create(new StringReader(wrapperProjectXml), xrs), projectRootElementCache, isExplicitlyLoaded); + ProjectRootElement projectRootElement = new ProjectRootElement(XmlReader.Create(new StringReader(wrapperProjectXml), xrs), projectRootElementCache, isExplicitlyLoaded, + preserveFormatting: false); projectRootElement.DirectoryPath = Path.GetDirectoryName(projectFile); ProjectInstance instance = new ProjectInstance(projectRootElement, globalProperties, toolsVersion, buildParameters, loggingService, projectBuildEventContext); return new ProjectInstance[] { instance }; diff --git a/src/XMakeBuildEngine/UnitTests/Evaluation/ProjectRootElementCache_Tests.cs b/src/XMakeBuildEngine/UnitTests/Evaluation/ProjectRootElementCache_Tests.cs index d22819ded34..65a170282e0 100644 --- a/src/XMakeBuildEngine/UnitTests/Evaluation/ProjectRootElementCache_Tests.cs +++ b/src/XMakeBuildEngine/UnitTests/Evaluation/ProjectRootElementCache_Tests.cs @@ -52,7 +52,7 @@ public void AddNull() { Assert.Throws(() => { - ProjectCollection.GlobalProjectCollection.ProjectRootElementCache.Get("c:\\foo", (p, c) => null, true); + ProjectCollection.GlobalProjectCollection.ProjectRootElementCache.Get("c:\\foo", (p, c) => null, true, false); } ); } @@ -64,7 +64,7 @@ public void AddUnsavedProject() { Assert.Throws(() => { - ProjectCollection.GlobalProjectCollection.ProjectRootElementCache.Get("c:\\foo", (p, c) => ProjectRootElement.Create("c:\\bar"), true); + ProjectCollection.GlobalProjectCollection.ProjectRootElementCache.Get("c:\\foo", (p, c) => ProjectRootElement.Create("c:\\bar"), true, false); } ); } @@ -75,7 +75,7 @@ public void AddUnsavedProject() public void AddEntry() { ProjectRootElement projectRootElement = ProjectRootElement.Create("c:\\foo"); - ProjectRootElement projectRootElement2 = ProjectCollection.GlobalProjectCollection.ProjectRootElementCache.Get("c:\\foo", (p, c) => { throw new InvalidOperationException(); }, true); + ProjectRootElement projectRootElement2 = ProjectCollection.GlobalProjectCollection.ProjectRootElementCache.Get("c:\\foo", (p, c) => { throw new InvalidOperationException(); }, true, false); Assert.Same(projectRootElement, projectRootElement2); } @@ -91,7 +91,7 @@ public void AddEntryStrongReference() projectRootElement = null; GC.Collect(); - projectRootElement = ProjectCollection.GlobalProjectCollection.ProjectRootElementCache.Get("c:\\foo", (p, c) => { throw new InvalidOperationException(); }, true); + projectRootElement = ProjectCollection.GlobalProjectCollection.ProjectRootElementCache.Get("c:\\foo", (p, c) => { throw new InvalidOperationException(); }, true, false); Assert.NotNull(projectRootElement); diff --git a/src/XMakeBuildEngine/UnitTestsPublicOM/Construction/ProjectFormatting_Tests.cs b/src/XMakeBuildEngine/UnitTestsPublicOM/Construction/ProjectFormatting_Tests.cs new file mode 100644 index 00000000000..29d8663fbcd --- /dev/null +++ b/src/XMakeBuildEngine/UnitTestsPublicOM/Construction/ProjectFormatting_Tests.cs @@ -0,0 +1,462 @@ +using Microsoft.Build.Construction; +using Microsoft.Build.Evaluation; +using Microsoft.Build.UnitTests; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Xml; +using Xunit; +using Xunit.Abstractions; + +namespace Microsoft.Build.Engine.OM.UnitTests.Construction +{ + public class ProjectFormatting_Tests : IDisposable + { + private readonly ITestOutputHelper _testOutput; + + public ProjectFormatting_Tests(ITestOutputHelper testOutput) + { + _testOutput = testOutput; + Setup(); + } + + /// + /// Clear out the cache + /// + public void Setup() + { + ProjectCollection.GlobalProjectCollection.UnloadAllProjects(); + GC.Collect(); + } + + /// + /// Clear out the cache + /// + public void Dispose() + { + Setup(); + } + + [Fact] + public void ProjectCommentFormatting() + { + string content = ObjectModelHelpers.CleanupFileContents(@" + + + + {3699f81b-2d03-46c5-abd7-e88a4c946f28} + + +"); + + string reformattedContent = ObjectModelHelpers.CleanupFileContents(@" + + + + + {3699f81b-2d03-46c5-abd7-e88a4c946f28} + + +"); + + VerifyFormattingPreserved(content); + VerifyProjectReformatting(content, reformattedContent); + } + + [Fact] + public void ProjectWhitespaceFormatting() + { + // Note that there are two spaces after the tag on the second line + string content = ObjectModelHelpers.CleanupFileContents(@" + + + +{3699f81b-2d03-46c5-abd7-e88a4c946f28} + + +"); + + string reformattedContent = ObjectModelHelpers.CleanupFileContents(@" + + + + {3699f81b-2d03-46c5-abd7-e88a4c946f28} + + +"); + + VerifyFormattingPreserved(content); + VerifyProjectReformatting(content, reformattedContent); + } + + [Fact] + public void ProjectAddItemFormatting_StartOfGroup() + { + string content = ObjectModelHelpers.CleanupFileContents(@" + + + + + +"); + + ProjectRootElement xml = ProjectRootElement.Create(XmlReader.Create(new StringReader(content)), + ProjectCollection.GlobalProjectCollection, + preserveFormatting: true); + Project project = new Project(xml); + project.AddItem("Compile", "Class1.cs"); + StringWriter writer = new StringWriter(); + project.Save(writer); + + string expected = ObjectModelHelpers.CleanupFileContents(@" + + + + + + +"); + + string actual = writer.ToString(); + + VerifyAssertLineByLine(expected, actual); + } + + [Fact] + public void ProjectAddItemFormatting_MiddleOfGroup() + { + string content = ObjectModelHelpers.CleanupFileContents(@" + + + + + +"); + + ProjectRootElement xml = ProjectRootElement.Create(XmlReader.Create(new StringReader(content)), + ProjectCollection.GlobalProjectCollection, + preserveFormatting: true); + Project project = new Project(xml); + project.AddItem("Compile", "Class2.cs"); + StringWriter writer = new StringWriter(); + project.Save(writer); + + string expected = ObjectModelHelpers.CleanupFileContents(@" + + + + + + +"); + + string actual = writer.ToString(); + + VerifyAssertLineByLine(expected, actual); + } + + [Fact] + public void ProjectAddItemFormatting_EndOfGroup() + { + string content = ObjectModelHelpers.CleanupFileContents(@" + + + + + +"); + + ProjectRootElement xml = ProjectRootElement.Create(XmlReader.Create(new StringReader(content)), + ProjectCollection.GlobalProjectCollection, + preserveFormatting: true); + Project project = new Project(xml); + project.AddItem("Compile", "Program.cs"); + StringWriter writer = new StringWriter(); + project.Save(writer); + + string expected = ObjectModelHelpers.CleanupFileContents(@" + + + + + + +"); + + string actual = writer.ToString(); + + VerifyAssertLineByLine(expected, actual); + } + + [Fact] + public void ProjectAddItemFormatting_EmptyGroup() + { + string content = ObjectModelHelpers.CleanupFileContents(@" + + + +"); + + ProjectRootElement xml = ProjectRootElement.Create(XmlReader.Create(new StringReader(content)), + ProjectCollection.GlobalProjectCollection, + preserveFormatting: true); + Project project = new Project(xml); + project.AddItem("Compile", "Program.cs"); + StringWriter writer = new StringWriter(); + project.Save(writer); + + string expected = ObjectModelHelpers.CleanupFileContents(@" + + + + +"); + + string actual = writer.ToString(); + + VerifyAssertLineByLine(expected, actual); + } + + [Fact] + public void ProjectAddItemFormatting_NoItemGroup() + { + string content = ObjectModelHelpers.CleanupFileContents(@" + +"); + + ProjectRootElement xml = ProjectRootElement.Create(XmlReader.Create(new StringReader(content)), + ProjectCollection.GlobalProjectCollection, + preserveFormatting: true); + Project project = new Project(xml); + project.AddItem("Compile", "Program.cs"); + StringWriter writer = new StringWriter(); + project.Save(writer); + + string expected = ObjectModelHelpers.CleanupFileContents(@" + + + + +"); + + string actual = writer.ToString(); + + VerifyAssertLineByLine(expected, actual); + } + + [Fact] + public void ProjectRemoveItemFormatting() + { + string content = ObjectModelHelpers.CleanupFileContents(@" + + + + + + +"); + + ProjectRootElement xml = ProjectRootElement.Create(XmlReader.Create(new StringReader(content)), + ProjectCollection.GlobalProjectCollection, + preserveFormatting: true); + Project project = new Project(xml); + + var itemToRemove = project.GetItems("Compile").Single(item => item.EvaluatedInclude == "Class2.cs"); + project.RemoveItem(itemToRemove); + + StringWriter writer = new StringWriter(); + project.Save(writer); + + string expected = ObjectModelHelpers.CleanupFileContents(@" + + + + + +"); + + string actual = writer.ToString(); + + VerifyAssertLineByLine(expected, actual); + } + + [Fact] + public void ProjectAddItemMetadataFormatting() + { + string content = ObjectModelHelpers.CleanupFileContents(@" + + + + + + +"); + + ProjectRootElement xml = ProjectRootElement.Create(XmlReader.Create(new StringReader(content)), + ProjectCollection.GlobalProjectCollection, + preserveFormatting: true); + Project project = new Project(xml); + + var itemToEdit = project.GetItems("Compile").Single(item => item.EvaluatedInclude == "Class2.cs"); + itemToEdit.SetMetadataValue("ExcludeFromStyleCop", "true"); + + StringWriter writer = new StringWriter(); + project.Save(writer); + + string expected = ObjectModelHelpers.CleanupFileContents(@" + + + + + true + + + +"); + + string actual = writer.ToString(); + + VerifyAssertLineByLine(expected, actual); + } + + [Fact(Skip = "https://github.com/Microsoft/msbuild/issues/362")] + public void PreprocessorFormatting() + { + string content = ObjectModelHelpers.CleanupFileContents(@" + + +"); + + ProjectRootElement xml = ProjectRootElement.Create(XmlReader.Create(new StringReader(content)), ProjectCollection.GlobalProjectCollection, + preserveFormatting: true); + Project project = new Project(xml); + + StringWriter writer = new StringWriter(); + + project.SaveLogicalProject(writer); + + string actual = writer.ToString(); + string expected = @"" + + content; + + VerifyAssertLineByLine(expected, actual); + } + + void VerifyFormattingPreserved(string projectContents) + { + VerifyFormattingPreservedFromString(projectContents); + VerifyFormattingPreservedFromFile(projectContents); + } + + void VerifyFormattingPreservedFromString(string projectContents) + { + ProjectRootElement xml = ProjectRootElement.Create(XmlReader.Create(new StringReader(projectContents)), + ProjectCollection.GlobalProjectCollection, + preserveFormatting: true); + Project project = new Project(xml); + StringWriter writer = new StringWriter(); + project.Save(writer); + + string expected = @"" + + projectContents; + string actual = writer.ToString(); + + VerifyAssertLineByLine(expected, actual); + } + + 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); + StringWriter writer = new StringWriter(); + project.Save(writer); + + string expected = @"" + + projectContents; + string actual = writer.ToString(); + + VerifyAssertLineByLine(expected, actual); + } + finally + { + Directory.Delete(directory, true); + } + } + + void VerifyProjectReformatting(string originalContents, string expectedContents) + { + VerifyProjectReformattingFromString(originalContents, expectedContents); + VerifyProjectReformattingFromFile(originalContents, expectedContents); + } + + void VerifyProjectReformattingFromString(string originalContents, string expectedContents) + { + ProjectRootElement xml = ProjectRootElement.Create(XmlReader.Create(new StringReader(originalContents)), + ProjectCollection.GlobalProjectCollection, + preserveFormatting: false); + Project project = new Project(xml); + StringWriter writer = new StringWriter(); + project.Save(writer); + + string expected = @"" + + expectedContents; + string actual = writer.ToString(); + + VerifyAssertLineByLine(expected, actual); + } + + 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); + StringWriter writer = new StringWriter(); + project.Save(writer); + + string expected = @"" + + expectedContents; + string actual = writer.ToString(); + + VerifyAssertLineByLine(expected, actual); + } + finally + { + Directory.Delete(directory, true); + } + } + + void VerifyAssertLineByLine(string expected, string actual) + { + Helpers.VerifyAssertLineByLine(expected, actual, false, _testOutput); + } + } +} diff --git a/src/XMakeBuildEngine/UnitTestsPublicOM/Microsoft.Build.Engine.OM.UnitTests.csproj b/src/XMakeBuildEngine/UnitTestsPublicOM/Microsoft.Build.Engine.OM.UnitTests.csproj index c3a765d3cf4..8eeb009b2d6 100644 --- a/src/XMakeBuildEngine/UnitTestsPublicOM/Microsoft.Build.Engine.OM.UnitTests.csproj +++ b/src/XMakeBuildEngine/UnitTestsPublicOM/Microsoft.Build.Engine.OM.UnitTests.csproj @@ -78,6 +78,7 @@ +