|
// 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.IO;
using System.Linq;
using System.Text.RegularExpressions;
using Microsoft.Build.Construction;
using Microsoft.Build.Evaluation;
using Microsoft.Build.Shared;
using Xunit;
#nullable disable
namespace Microsoft.Build.UnitTests.OM.Construction
{
public class WhitespacePreservation_Tests
{
[Theory]
[InlineData(
@"<Project xmlns=`msbuildnamespace`>
</Project>",
@"<Project xmlns=`msbuildnamespace`>
<ItemGroup />
</Project>")]
[InlineData(
@"<Project xmlns=`msbuildnamespace`>
</Project>",
@"<Project xmlns=`msbuildnamespace`>
<ItemGroup />
</Project>")]
public void AddEmptyParent(string projectContents, string updatedProject)
{
AssertWhiteSpacePreservation(projectContents, updatedProject, (pe, p) =>
{
pe.AddItemGroup();
Assert.True(p.IsDirty);
});
}
[Theory]
[InlineData(
@"<Project xmlns=`msbuildnamespace`>
<ItemGroup>
<i Include=`a` />
</ItemGroup>
</Project>",
@"<Project xmlns=`msbuildnamespace`>
<ItemGroup>
<i Include=`a` />
</ItemGroup>
<ItemGroup>
<i2 Include=`b` />
</ItemGroup>
</Project>")]
[InlineData(
@"<Project xmlns=`msbuildnamespace`>
<ItemGroup>
<i Include=`a` />
</ItemGroup>
</Project>",
@"<Project xmlns=`msbuildnamespace`>
<ItemGroup>
<i Include=`a` />
</ItemGroup>
<ItemGroup>
<i2 Include=`b` />
</ItemGroup>
</Project>")]
public void AddParentAndChild(string projectContents, string updatedProject)
{
AssertWhiteSpacePreservation(projectContents, updatedProject, (pe, p) =>
{
var itemGroup = pe.AddItemGroup();
itemGroup.AddItem("i2", "b");
Assert.True(p.IsDirty);
});
}
[Theory]
// no new lines are added
[InlineData(
@"<Project xmlns=`msbuildnamespace`>
<ItemGroup>
<i Include=`a` />
</ItemGroup>
<ItemGroup>
</ItemGroup>
</Project>",
@"<Project xmlns=`msbuildnamespace`>
<ItemGroup>
<i Include=`a` />
</ItemGroup>
<ItemGroup>
<i2 Include=`b` />
</ItemGroup>
</Project>")]
// new lines between parents are preserved
[InlineData(
@"<Project xmlns=`msbuildnamespace`>
<ItemGroup>
<i Include=`a` />
</ItemGroup>
<ItemGroup>
</ItemGroup>
</Project>",
@"<Project xmlns=`msbuildnamespace`>
<ItemGroup>
<i Include=`a` />
</ItemGroup>
<ItemGroup>
<i2 Include=`b` />
</ItemGroup>
</Project>")]
// parent has no indentation but has leading whitespace. Indentation is the whitespace after the last new line in the parent's entire leading whitespace
[InlineData(
@"<Project xmlns=`msbuildnamespace`>
<ItemGroup>
<i Include=`a` />
</ItemGroup> <ItemGroup>
</ItemGroup>
</Project>",
@"<Project xmlns=`msbuildnamespace`>
<ItemGroup>
<i Include=`a` />
</ItemGroup> <ItemGroup>
<i2 Include=`b` />
</ItemGroup>
</Project>")]
// parent has no leading whitespace
[InlineData(
@"<Project xmlns=`msbuildnamespace`>
<ItemGroup>
<i Include=`a` />
</ItemGroup><ItemGroup>
</ItemGroup>
</Project>",
@"<Project xmlns=`msbuildnamespace`>
<ItemGroup>
<i Include=`a` />
</ItemGroup><ItemGroup>
<i2 Include=`b` />
</ItemGroup>
</Project>")]
// empty parent has no whitespace in it; append new line and the parent's indentation
[InlineData(
@"<Project xmlns=`msbuildnamespace`>
<ItemGroup>
<i Include=`a` />
</ItemGroup>
<ItemGroup></ItemGroup>
</Project>",
@"<Project xmlns=`msbuildnamespace`>
<ItemGroup>
<i Include=`a` />
</ItemGroup>
<ItemGroup>
<i2 Include=`b` />
</ItemGroup>
</Project>")]
// the initial whitespace in the empty parent gets replaced with newline + parent_indentation
[InlineData(
@"<Project xmlns=`msbuildnamespace`>
<ItemGroup>
<i Include=`a` />
</ItemGroup>
<ItemGroup>
</ItemGroup>
</Project>",
@"<Project xmlns=`msbuildnamespace`>
<ItemGroup>
<i Include=`a` />
</ItemGroup>
<ItemGroup>
<i2 Include=`b` />
</ItemGroup>
</Project>")]
public void AddFirstChildInExistingParent(string projectContents, string updatedProject)
{
AssertWhiteSpacePreservation(projectContents, updatedProject,
(pe, p) => { pe.ItemGroups.ElementAt(1).AddItem("i2", "b"); });
}
[Theory]
[InlineData(
@"<Project xmlns=`msbuildnamespace`>
<ItemGroup>
<i Include=`a` />
</ItemGroup>
</Project>",
@"<Project xmlns=`msbuildnamespace`>
<ItemGroup>
<i Include=`a` />
<i2 Include=`b` />
</ItemGroup>
</Project>")]
[InlineData(
@"<Project xmlns=`msbuildnamespace`>
<ItemGroup>
<i Include=`a` />
</ItemGroup>
</Project>",
@"<Project xmlns=`msbuildnamespace`>
<ItemGroup>
<i Include=`a` />
<i2 Include=`b` />
</ItemGroup>
</Project>")]
[InlineData(
@"<Project xmlns=`msbuildnamespace`>
<ItemGroup>
<i Include=`a` />
</ItemGroup>
</Project>",
@"<Project xmlns=`msbuildnamespace`>
<ItemGroup>
<i Include=`a` />
<i2 Include=`b` />
</ItemGroup>
</Project>")]
[InlineData(
@"<Project xmlns=`msbuildnamespace`>
<ItemGroup>
<i Include=`a` />
</ItemGroup>
</Project>",
@"<Project xmlns=`msbuildnamespace`>
<ItemGroup>
<i Include=`a` />
<i2 Include=`b` />
</ItemGroup>
</Project>")]
// AddItem ends up calling InsertAfterChild
public void AddChildWithExistingSiblingsViaAddItem(string projectContents, string updatedProject)
{
AssertWhiteSpacePreservation(projectContents, updatedProject,
(pe, p) => { pe.ItemGroups.First().AddItem("i2", "b"); });
}
[Theory]
[InlineData(
@"<Project xmlns=`msbuildnamespace`>
<ItemGroup>
<i Include=`a` />
</ItemGroup>
</Project>",
@"<Project xmlns=`msbuildnamespace`>
<ItemGroup>
<i2 Include=`b` />
<i Include=`a` />
</ItemGroup>
</Project>")]
[InlineData(
@"<Project xmlns=`msbuildnamespace`>
<ItemGroup>
<i Include=`a` />
</ItemGroup>
</Project>",
@"<Project xmlns=`msbuildnamespace`>
<ItemGroup>
<i2 Include=`b` />
<i Include=`a` />
</ItemGroup>
</Project>")]
[InlineData(
@"<Project xmlns=`msbuildnamespace`>
<ItemGroup>
<i Include=`a` />
</ItemGroup>
</Project>",
@"<Project xmlns=`msbuildnamespace`>
<ItemGroup>
<i2 Include=`b` />
<i Include=`a` />
</ItemGroup>
</Project>")]
[InlineData(
@"<Project xmlns=`msbuildnamespace`>
<ItemGroup>
<i Include=`a` />
</ItemGroup>
</Project>",
@"<Project xmlns=`msbuildnamespace`>
<ItemGroup>
<i2 Include=`b` />
<i Include=`a` />
</ItemGroup>
</Project>")]
public void AddChildWithExistingSiblingsViaInsertBeforeChild(string projectContents, string updatedProject)
{
AssertWhiteSpacePreservation(projectContents, updatedProject,
(pe, p) =>
{
var itemGroup = pe.ItemGroups.First();
var existingItemElement = itemGroup.FirstChild;
var newItemElement = itemGroup.ContainingProject.CreateItemElement("i2", "b");
itemGroup.InsertBeforeChild(newItemElement, existingItemElement);
});
}
[Fact]
public void VerifySaveProjectContainsCorrectLineEndings()
{
var project = @"<Project xmlns=`msbuildnamespace`>
<ItemGroup> <!-- comment here -->
<i Include=`a` /> <!--
multi-line comment here
-->
</ItemGroup>
</Project>
";
string expected = @"<Project xmlns=`msbuildnamespace`>
<ItemGroup> <!-- comment here -->
<i2 Include=`b` />
<i Include=`a` /> <!--
multi-line comment here
-->
</ItemGroup>
</Project>
";
// Use existing test to add a sibling and verify the output is as expected (including comments)
AddChildWithExistingSiblingsViaInsertBeforeChild(project, expected);
}
private void AssertWhiteSpacePreservation(
string projectContents,
string updatedProject,
Action<ProjectRootElement, Project> act)
{
// Each OS uses its own line endings. Using WSL on Windows leads to LF on Windows which messes up the tests. This happens due to git LF <-> CRLF conversions.
if (NativeMethodsShared.IsWindows)
{
projectContents = Regex.Replace(projectContents, @"(?<!\r)\n", "\r\n", RegexOptions.Multiline);
updatedProject = Regex.Replace(updatedProject, @"(?<!\r)\n", "\r\n", RegexOptions.Multiline);
}
else
{
projectContents = Regex.Replace(projectContents, @"\r\n", "\n", RegexOptions.Multiline);
updatedProject = Regex.Replace(updatedProject, @"\r\n", "\n", RegexOptions.Multiline);
}
// Note: This test will write the project file to disk rather than using in-memory streams.
// Using streams can cause issues with CRLF characters being replaced by LF going in to
// ProjectRootElement. Saving to disk mimics the real-world behavior so we can specifically
// test issues with CRLF characters being normalized. Related issue: #1340
var file = FileUtilities.GetTemporaryFileName();
var expected = ObjectModelHelpers.CleanupFileContents(updatedProject);
string actual;
try
{
// Write the projectConents to disk and load it
File.WriteAllText(file, ObjectModelHelpers.CleanupFileContents(projectContents));
var projectElement = ProjectRootElement.Open(file, ProjectCollection.GlobalProjectCollection, true);
var project = new Project(projectElement);
act(projectElement, project);
// Write the project to a UTF8 string writer to compare against
using var writer = new EncodingStringWriter();
project.Save(writer);
actual = writer.ToString();
}
finally
{
FileUtilities.DeleteNoThrow(file);
}
VerifyAssertLineByLine(expected, actual);
VerifyLineEndings(actual);
}
private void VerifyAssertLineByLine(string expected, string actual)
{
Helpers.VerifyAssertLineByLine(expected, actual, false);
}
/// <summary>
/// Ensure that all line-endings in the save result are correct for the current OS
/// </summary>
/// <param name="projectResults">Project file contents after save.</param>
private void VerifyLineEndings(string projectResults)
{
if (Environment.NewLine.Length == 2)
{
// Windows, ensure that \n doesn't exist by itself
var crlfCount = Regex.Matches(projectResults, @"\r\n", RegexOptions.Multiline).Count;
var nlCount = Regex.Matches(projectResults, @"\n").Count;
// Compare number of \r\n to number of \n, they should be equal.
Assert.Equal(crlfCount, nlCount);
}
else
{
// Ensure we did not add \r\n
Assert.Empty(Regex.Matches(projectResults, @"\r\n", RegexOptions.Multiline));
}
}
}
}
|