diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Run/RunWorkerTests.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Run/RunWorkerTests.cs index e13bc0a6fd9..a38b2746682 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Run/RunWorkerTests.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Run/RunWorkerTests.cs @@ -14,12 +14,17 @@ namespace NuGetUpdater.Core.Test.Run; +using static NuGetUpdater.Core.Utilities.EOLHandling; + using TestFile = (string Path, string Content); public class RunWorkerTests { - [Fact] - public async Task UpdateSinglePackageProducedExpectedAPIMessages() + [Theory] + [InlineData(EOLType.CR)] + [InlineData(EOLType.LF)] + [InlineData(EOLType.CRLF)] + public async Task UpdateSinglePackageProducedExpectedAPIMessages(EOLType EOL) { await RunAsync( packages: [], @@ -43,7 +48,7 @@ await RunAsync( - """) + """.SetEOL(EOL)) ], discoveryWorker: new TestDiscoveryWorker(_input => { @@ -94,7 +99,7 @@ await File.WriteAllTextAsync(projectPath, """ - """); + """.SetEOL(EOL)); return new UpdateOperationResult(); }), expectedResult: new RunResult() @@ -114,7 +119,7 @@ await File.WriteAllTextAsync(projectPath, """ - """)) + """.SetEOL(EOL))) } ], BaseCommitSha = "TEST-COMMIT-SHA", @@ -199,7 +204,7 @@ await File.WriteAllTextAsync(projectPath, """ - """, + """.SetEOL(EOL), }, ], BaseCommitSha = "TEST-COMMIT-SHA", @@ -212,11 +217,14 @@ await File.WriteAllTextAsync(projectPath, """ ); } - [Fact] - public async Task UpdateHandlesSemicolonsInPackageReference() + [Theory] + [InlineData(EOLType.CR)] + [InlineData(EOLType.LF)] + [InlineData(EOLType.CRLF)] + public async Task UpdateHandlesSemicolonsInPackageReference(EOLType EOL) { - var repoMetadata = XElement.Parse(""""""); - var repoMetadata2 = XElement.Parse(""""""); + var repoMetadata = XElement.Parse("""""".SetEOL(EOL)); + var repoMetadata2 = XElement.Parse("""""".SetEOL(EOL)); await RunAsync( packages: [ @@ -246,7 +254,7 @@ await RunAsync( - """) + """.SetEOL(EOL)) ], discoveryWorker: new TestDiscoveryWorker(_input => { @@ -299,7 +307,7 @@ await File.WriteAllTextAsync(projectPath, """ - """); + """.SetEOL(EOL)); return new UpdateOperationResult(); }), expectedResult: new RunResult() @@ -319,7 +327,7 @@ await File.WriteAllTextAsync(projectPath, """ - """)) + """.SetEOL(EOL))) } ], BaseCommitSha = "TEST-COMMIT-SHA", @@ -447,7 +455,7 @@ await File.WriteAllTextAsync(projectPath, """ - """, + """.SetEOL(EOL), } ], @@ -461,8 +469,11 @@ await File.WriteAllTextAsync(projectPath, """ ); } - [Fact] - public async Task PrivateSourceAuthenticationFailureIsForwaredToApiHandler() + [Theory] + [InlineData(EOLType.CR)] + [InlineData(EOLType.LF)] + [InlineData(EOLType.CRLF)] + public async Task PrivateSourceAuthenticationFailureIsForwaredToApiHandler(EOLType EOL) { await RunAsync( packages: @@ -486,7 +497,7 @@ await RunAsync( - """), + """.SetEOL(EOL)), ("project.csproj", """ @@ -496,7 +507,7 @@ await RunAsync( - """) + """.SetEOL(EOL)) ], discoveryWorker: new TestDiscoveryWorker((_input) => { @@ -517,11 +528,14 @@ await RunAsync( ); } - [Fact] - public async Task UpdateHandlesPackagesConfigFiles() + [Theory] + [InlineData(EOLType.CR)] + [InlineData(EOLType.LF)] + [InlineData(EOLType.CRLF)] + public async Task UpdateHandlesPackagesConfigFiles(EOLType EOL) { - var repoMetadata = XElement.Parse(""""""); - var repoMetadata2 = XElement.Parse(""""""); + var repoMetadata = XElement.Parse("""""".SetEOL(EOL)); + var repoMetadata2 = XElement.Parse("""""".SetEOL(EOL)); await RunAsync( packages: [ @@ -551,13 +565,13 @@ await RunAsync( - """), + """.SetEOL(EOL)), ("some-dir/packages.config", """ - """), + """.SetEOL(EOL)), ], discoveryWorker: new TestDiscoveryWorker(_input => { @@ -630,7 +644,7 @@ await File.WriteAllTextAsync(projectPath, """ - """); + """.SetEOL(EOL)); break; case "Some.Package2": await File.WriteAllTextAsync(projectPath, """ @@ -648,14 +662,14 @@ await File.WriteAllTextAsync(projectPath, """ - """); + """.SetEOL(EOL)); var packagesConfigPath = Path.Join(Path.GetDirectoryName(projectPath)!, "packages.config"); await File.WriteAllTextAsync(packagesConfigPath, """ - """); + """.SetEOL(EOL)); break; default: throw new NotSupportedException(); @@ -676,7 +690,7 @@ await File.WriteAllTextAsync(packagesConfigPath, """ - """)) + """.SetEOL(EOL))) }, new DependencyFile() { @@ -691,7 +705,7 @@ await File.WriteAllTextAsync(packagesConfigPath, """ - """)) + """.SetEOL(EOL))) }, ], BaseCommitSha = "TEST-COMMIT-SHA", @@ -815,7 +829,7 @@ await File.WriteAllTextAsync(packagesConfigPath, """ - """, + """.SetEOL(EOL), }, new DependencyFile() { @@ -836,7 +850,7 @@ await File.WriteAllTextAsync(packagesConfigPath, """ - """, + """.SetEOL(EOL), }, ], BaseCommitSha = "TEST-COMMIT-SHA", @@ -849,11 +863,14 @@ await File.WriteAllTextAsync(packagesConfigPath, """ ); } - [Fact] - public async Task UpdateHandlesPackagesConfigFromReferencedCsprojFiles() + [Theory] + [InlineData(EOLType.CR)] + [InlineData(EOLType.LF)] + [InlineData(EOLType.CRLF)] + public async Task UpdateHandlesPackagesConfigFromReferencedCsprojFiles(EOLType EOL) { - var repoMetadata = XElement.Parse(""""""); - var repoMetadata2 = XElement.Parse(""""""); + var repoMetadata = XElement.Parse("""""".SetEOL(EOL)); + var repoMetadata2 = XElement.Parse("""""".SetEOL(EOL)); await RunAsync( packages: [ @@ -886,13 +903,13 @@ await RunAsync( - """), + """.SetEOL(EOL)), ("some-dir/ProjectA/packages.config", """ - """), + """.SetEOL(EOL)), ("some-dir/ProjectB/ProjectB.csproj", """ @@ -902,13 +919,13 @@ await RunAsync( - """), + """.SetEOL(EOL)), ("some-dir/ProjectB/packages.config", """ - """), + """.SetEOL(EOL)), ], discoveryWorker: new TestDiscoveryWorker(_input => { @@ -999,7 +1016,7 @@ await File.WriteAllTextAsync(projectPath, """ - """); + """.SetEOL(EOL)); break; case ("ProjectA.csproj", "Some.Package2"): await File.WriteAllTextAsync(projectPath, """ @@ -1020,13 +1037,13 @@ await File.WriteAllTextAsync(projectPath, """ - """); + """.SetEOL(EOL)); await File.WriteAllTextAsync(packagesConfigPath, """ - """); + """.SetEOL(EOL)); break; case ("ProjectB.csproj", "Some.Package"): await File.WriteAllTextAsync(projectPath, """ @@ -1038,7 +1055,7 @@ await File.WriteAllTextAsync(projectPath, """ - """); + """.SetEOL(EOL)); break; case ("ProjectB.csproj", "Some.Package2"): await File.WriteAllTextAsync(projectPath, """ @@ -1056,13 +1073,13 @@ await File.WriteAllTextAsync(projectPath, """ - """); + """.SetEOL(EOL)); await File.WriteAllTextAsync(packagesConfigPath, """ - """); + """.SetEOL(EOL)); break; default: throw new NotSupportedException(); @@ -1083,7 +1100,7 @@ await File.WriteAllTextAsync(packagesConfigPath, """ - """)) + """.SetEOL(EOL))) }, new DependencyFile() { @@ -1101,7 +1118,7 @@ await File.WriteAllTextAsync(packagesConfigPath, """ - """)) + """.SetEOL(EOL))) }, new DependencyFile() { @@ -1112,7 +1129,7 @@ await File.WriteAllTextAsync(packagesConfigPath, """ - """)) + """.SetEOL(EOL))) }, new DependencyFile() { @@ -1127,7 +1144,7 @@ await File.WriteAllTextAsync(packagesConfigPath, """ - """)) + """.SetEOL(EOL))) }, ], BaseCommitSha = "TEST-COMMIT-SHA", @@ -1337,7 +1354,7 @@ await File.WriteAllTextAsync(packagesConfigPath, """ - """, + """.SetEOL(EOL), }, new DependencyFile() { @@ -1361,7 +1378,7 @@ await File.WriteAllTextAsync(packagesConfigPath, """ - """, + """.SetEOL(EOL), }, new DependencyFile() { @@ -1372,7 +1389,7 @@ await File.WriteAllTextAsync(packagesConfigPath, """ - """, + """.SetEOL(EOL), }, new DependencyFile() { @@ -1393,7 +1410,7 @@ await File.WriteAllTextAsync(packagesConfigPath, """ - """, + """.SetEOL(EOL), }, ], BaseCommitSha = "TEST-COMMIT-SHA", @@ -1406,8 +1423,11 @@ await File.WriteAllTextAsync(packagesConfigPath, """ ); } - [Fact] - public async Task UpdatedFilesAreOnlyReportedOnce() + [Theory] + [InlineData(EOLType.CR)] + [InlineData(EOLType.LF)] + [InlineData(EOLType.CRLF)] + public async Task UpdatedFilesAreOnlyReportedOnce(EOLType EOL) { await RunAsync( job: new() @@ -1434,14 +1454,14 @@ await RunAsync( - """), + """.SetEOL(EOL)), ("Directory.Build.props", """ 1.0.0 - """), + """.SetEOL(EOL)), ("project1/project1.csproj", """ @@ -1451,7 +1471,7 @@ await RunAsync( - """), + """.SetEOL(EOL)), ("project2/project2.csproj", """ @@ -1461,7 +1481,7 @@ await RunAsync( - """) + """.SetEOL(EOL)) ], discoveryWorker: new TestDiscoveryWorker(_input => { @@ -1526,7 +1546,7 @@ await File.WriteAllTextAsync(directoryBuildPropsPath, """ 1.1.0 - """); + """.SetEOL(EOL)); return new UpdateOperationResult(); }), expectedResult: new RunResult() @@ -1543,7 +1563,7 @@ await File.WriteAllTextAsync(directoryBuildPropsPath, """ 1.0.0 - """)) + """.SetEOL(EOL))) }, new DependencyFile() { @@ -1558,7 +1578,7 @@ await File.WriteAllTextAsync(directoryBuildPropsPath, """ - """)) + """.SetEOL(EOL))) }, new DependencyFile() { @@ -1573,7 +1593,7 @@ await File.WriteAllTextAsync(directoryBuildPropsPath, """ - """)) + """.SetEOL(EOL))) }, ], BaseCommitSha = "TEST-COMMIT-SHA", @@ -1698,7 +1718,7 @@ await File.WriteAllTextAsync(directoryBuildPropsPath, """ 1.1.0 - """, + """.SetEOL(EOL), } ], BaseCommitSha = "TEST-COMMIT-SHA", @@ -1711,8 +1731,11 @@ await File.WriteAllTextAsync(directoryBuildPropsPath, """ ); } - [Fact] - public async Task UpdatePackageWithDifferentVersionsInDifferentDirectories() + [Theory] + [InlineData(EOLType.CR)] + [InlineData(EOLType.LF)] + [InlineData(EOLType.CRLF)] + public async Task UpdatePackageWithDifferentVersionsInDifferentDirectories(EOLType EOL) { // this test passes `null` for discovery, analyze, and update workers to fully test the desired behavior @@ -1754,7 +1777,7 @@ await RunAsync( - """), + """.SetEOL(EOL)), ("Directory.Build.props", ""), ("Directory.Build.targets", ""), ("Directory.Packages.props", """ @@ -1763,7 +1786,7 @@ await RunAsync( false - """), + """.SetEOL(EOL)), ("library1/library1.csproj", """ @@ -1773,7 +1796,7 @@ await RunAsync( - """), + """.SetEOL(EOL)), ("library2/library2.csproj", """ @@ -1783,7 +1806,7 @@ await RunAsync( - """), + """.SetEOL(EOL)), ("library3/library3.csproj", """ @@ -1793,7 +1816,7 @@ await RunAsync( - """), + """.SetEOL(EOL)), ], discoveryWorker: null, analyzeWorker: null, @@ -1824,7 +1847,7 @@ await RunAsync( false - """)) + """.SetEOL(EOL))) }, new DependencyFile() { @@ -1839,7 +1862,7 @@ await RunAsync( - """)) + """.SetEOL(EOL))) }, new DependencyFile() { @@ -1854,7 +1877,7 @@ await RunAsync( - """)) + """.SetEOL(EOL))) }, new DependencyFile() { @@ -1869,7 +1892,7 @@ await RunAsync( - """)) + """.SetEOL(EOL))) } ], BaseCommitSha = "TEST-COMMIT-SHA", @@ -2020,7 +2043,7 @@ await RunAsync( - """ + """.SetEOL(EOL) }, new() { @@ -2036,7 +2059,7 @@ await RunAsync( - """ + """.SetEOL(EOL) } ], BaseCommitSha = "TEST-COMMIT-SHA", diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Utilities/EOLHandlingTests.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Utilities/EOLHandlingTests.cs new file mode 100644 index 00000000000..ad3c9c194c4 --- /dev/null +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Utilities/EOLHandlingTests.cs @@ -0,0 +1,23 @@ +using System.Text.RegularExpressions; + +using Xunit; + +using static NuGetUpdater.Core.Utilities.EOLHandling; + +namespace NuGetUpdater.Core.Test.Utilities +{ + public class EOLHandlingTests + { + [Theory] + [InlineData(EOLType.LF, "\n")] + [InlineData(EOLType.CR, "\r")] + [InlineData(EOLType.CRLF, "\r\n")] + public void ValidateEOLNormalizesFromLF(EOLType eolType, string literal) + { + var teststring = "this\ris\na\r\nstring\rwith\nmixed\r\nline\rendings\n."; + var changed = teststring.SetEOL(eolType); + var lineEndings = Regex.Split(changed, "\\S+"); + Assert.All(lineEndings, lineEnding => lineEnding.Equals(literal)); + } + } +} diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Run/RunWorker.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Run/RunWorker.cs index 3b020d68edc..2a5f3773b7a 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Run/RunWorker.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Run/RunWorker.cs @@ -3,6 +3,7 @@ using System.Text; using System.Text.Json; using System.Text.Json.Serialization; +using System.Text.RegularExpressions; using Microsoft.Extensions.FileSystemGlobbing; @@ -13,6 +14,8 @@ using NuGetUpdater.Core.Run.ApiModel; using NuGetUpdater.Core.Utilities; +using static NuGetUpdater.Core.Utilities.EOLHandling; + namespace NuGetUpdater.Core.Run; public class RunWorker @@ -122,6 +125,7 @@ private async Task RunForDirectory(Job job, DirectoryInfo repoContent // TODO: pull out relevant dependencies, then check each for updates and track the changes var originalDependencyFileContents = new Dictionary(); + var originalDependencyFileEOFs = new Dictionary(); var actualUpdatedDependencies = new List(); // track original contents for later handling @@ -131,6 +135,7 @@ async Task TrackOriginalContentsAsync(string directory, string fileName) var localFullPath = Path.Join(repoContentsPath.FullName, repoFullPath); var content = await File.ReadAllTextAsync(localFullPath); originalDependencyFileContents[repoFullPath] = content; + originalDependencyFileEOFs[repoFullPath] = content.GetPredominantEOL(); } foreach (var project in discoveryResult.Projects) @@ -203,8 +208,13 @@ async Task AddUpdatedFileIfDifferentAsync(string directory, string fileName) var localFullPath = Path.GetFullPath(Path.Join(repoContentsPath.FullName, repoFullPath)); var originalContent = originalDependencyFileContents[repoFullPath]; var updatedContent = await File.ReadAllTextAsync(localFullPath); + + updatedContent = updatedContent.SetEOL(originalDependencyFileEOFs[repoFullPath]); + await File.WriteAllTextAsync(localFullPath, updatedContent); + if (updatedContent != originalContent) { + updatedDependencyFiles[localFullPath] = new DependencyFile() { Name = Path.GetFileName(repoFullPath), diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Utilities/EOLHandling.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Utilities/EOLHandling.cs new file mode 100644 index 00000000000..33339c9c8da --- /dev/null +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Utilities/EOLHandling.cs @@ -0,0 +1,78 @@ +using System.Text.RegularExpressions; + +namespace NuGetUpdater.Core.Utilities; + +public static class EOLHandling +{ + /// + /// Used to save (and then restore) which line endings are predominant in a file. + /// + public enum EOLType + { + /// + /// Line feed - \n + /// Typical on most systems. + /// + LF, + /// + /// Carriage return - \r + /// Typical on older MacOS, unlikely (but possible) to come up here + /// + CR, + /// + /// Carriage return and line feed - \r\n. + /// Typical on Windows + /// + CRLF + }; + + /// + /// Analyze the input string and find the most common line ending type. + /// + /// The string to analyze + /// The most common type of line ending in the input string. + public static EOLType GetPredominantEOL(this string content) + { + // Get stats on EOL characters/character sequences, if one predominates choose that for writing later. + var lfcount = content.Count(c => c == '\n'); + var crcount = content.Count(c => c == '\r'); + var crlfcount = Regex.Matches(content, "\r\n").Count(); + + // Since CRLF contains both a CR and a LF, subtract it from those counts + lfcount -= crlfcount; + crcount -= crlfcount; + if (crcount > lfcount && crcount > crlfcount) + { + return EOLType.CR; + } + else if (crlfcount > lfcount) + { + return EOLType.CRLF; + } + else + { + return EOLType.LF; + } + } + + /// + /// Given a line ending, modify the input string to uniformly use that line ending. + /// + /// The input string, which may have any combination of line endings. + /// The line ending type to use across the result. + /// The string with any line endings swapped to the desired type. + /// If EOLType is an unexpected value. + public static string SetEOL(this string content, EOLType desiredEOL) + { + switch (desiredEOL) + { + case EOLType.LF: + return Regex.Replace(content, "(\r\n|\r)", "\n"); + case EOLType.CR: + return Regex.Replace(content, "(\r\n|\n)", "\r"); + case EOLType.CRLF: + return Regex.Replace(content, "(\r\n|\r|\n)", "\r\n"); + } + throw new ArgumentOutOfRangeException(nameof(desiredEOL)); + } +}