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 @@
+