diff --git a/.github/workflows/CI-CD.yml b/.github/workflows/CI-CD.yml index eccc67d9..3e6030c6 100644 --- a/.github/workflows/CI-CD.yml +++ b/.github/workflows/CI-CD.yml @@ -25,16 +25,29 @@ jobs: os: [windows-latest, ubuntu-latest] runs-on: ${{ matrix.os }} + env: + DOTNET_INSTALL_DIR: "./.dotnet" + steps: - name: Checkout uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: fetch-depth: '0' + - name: Install Mono (Linux only) + if: matrix.os == 'ubuntu-latest' + run: | + sudo apt update + sudo apt install -y mono-complete + - name: Install .NET Core uses: actions/setup-dotnet@3e891b0cb619bf60e2c25674b222b8940e2c1c25 # v4.1.0 with: dotnet-version: 6.0.x + + - name: Add .NET to PATH (Linux only) + if: matrix.os == 'ubuntu-latest' + run: echo "$DOTNET_INSTALL_DIR:$DOTNET_INSTALL_DIR/tools" >> $GITHUB_PATH - name: Set up Visual Studio shell uses: egor-tensin/vs-shell@9a932a62d05192eae18ca370155cf877eecc2202 # v2 diff --git a/SIL.BuildTasks/UnitTestTasks/NUnit3.cs b/SIL.BuildTasks/UnitTestTasks/NUnit3.cs index 08036c4d..02f4eee5 100644 --- a/SIL.BuildTasks/UnitTestTasks/NUnit3.cs +++ b/SIL.BuildTasks/UnitTestTasks/NUnit3.cs @@ -4,7 +4,6 @@ using System; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; -using System.IO; using System.Text; namespace SIL.BuildTasks.UnitTestTasks diff --git a/SIL.ReleaseTasks.Tests/CreateReleaseNotesHtmlTests.cs b/SIL.ReleaseTasks.Tests/CreateReleaseNotesHtmlTests.cs index 62d3bdfa..f193a18c 100644 --- a/SIL.ReleaseTasks.Tests/CreateReleaseNotesHtmlTests.cs +++ b/SIL.ReleaseTasks.Tests/CreateReleaseNotesHtmlTests.cs @@ -1,54 +1,67 @@ -// Copyright (c) 2018 SIL Global +// Copyright (c) 2018-2025 SIL Global // This software is licensed under the MIT License (http://opensource.org/licenses/MIT) +using System; +using System.Collections.Generic; using System.IO; using NUnit.Framework; +using static System.IO.Path; namespace SIL.ReleaseTasks.Tests { [TestFixture] public class CreateReleaseNotesHtmlTests { + private const string kNotReleaseNotesClassName = "notmarkdown"; + private static readonly string NotReleaseNotesDiv = + $"
"; + private static readonly string NotReleaseNotesDivXpath = + $"//div[contains(@class, '{kNotReleaseNotesClassName}')]"; + private static readonly string ReleaseNotesClassAttribute = + $"class='{CreateReleaseNotesHtml.kReleaseNotesClassName}'"; + + private static string GetRandomFileEndingWith(string ending) => Combine(GetTempPath(), GetRandomFileName() + ending); + [Test] public void MissingMarkdownReturnsFalse() { var mockEngine = new MockEngine(); - var testMarkdown = new CreateReleaseNotesHtml(); - testMarkdown.ChangelogFile = Path.GetRandomFileName(); - testMarkdown.BuildEngine = mockEngine; - Assert.That(testMarkdown.Execute(), Is.False); + var sut = new CreateReleaseNotesHtml + { + ChangelogFile = GetRandomFileName(), + BuildEngine = mockEngine + }; + Assert.That(sut.Execute(), Is.False); Assert.That(mockEngine.LoggedMessages[0], Does.StartWith("The given markdown file (").And.EndsWith(") does not exist.")); } [Test] public void SimpleMdResultsInSimpleHtml() { - var testMarkdown = new CreateReleaseNotesHtml(); - using( - var filesForTest = new TwoTempFilesForTest(Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()+".Test.md"), - Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()+".Test.htm"))) + var sut = new CreateReleaseNotesHtml(); + using (var filesForTest = new TwoTempFilesForTest(GetRandomFileEndingWith(".Test.md"), + GetRandomFileEndingWith(".Test.htm"))) { File.WriteAllLines(filesForTest.FirstFile, new[] {"## 2.3.9", "* with some random content", "* does some things", "## 2.3.7", "* more", "## 2.2.2", "* things"}); - testMarkdown.ChangelogFile = filesForTest.FirstFile; - testMarkdown.HtmlFile = filesForTest.SecondFile; - Assert.That(testMarkdown.Execute(), Is.True); + sut.ChangelogFile = filesForTest.FirstFile; + sut.HtmlFile = filesForTest.SecondFile; + Assert.That(sut.Execute(), Is.True); } } [Test] - public void RemovesKACHead() + public void RemovesKeepAChangelogHead() { - var testMarkdown = new CreateReleaseNotesHtml(); - using( - var filesForTest = new TwoTempFilesForTest(Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()+".Test.md"), - Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()+".Test.htm"))) + var sut = new CreateReleaseNotesHtml(); + using (var filesForTest = new TwoTempFilesForTest(GetRandomFileEndingWith(".Test.md"), + GetRandomFileEndingWith(".Test.htm"))) { string changelogContent = @"# Change Log -All notable changes to this project will be documented in this file. +All notable changes to this project™ will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/) and this project adheres to [Semantic Versioning](http://semver.org/). @@ -78,8 +91,9 @@ and this project adheres to [Semantic Versioning](http://semver.org/). - All bugs."; string expectedHtml = -@"
-

[2.3.4] - 2020-12-09

+$"
" + +Environment.NewLine + +@"

[2.3.4] - 2020-12-09

Changed

  • This to that.
  • @@ -96,87 +110,110 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
"; File.WriteAllText(filesForTest.FirstFile, changelogContent); - testMarkdown.ChangelogFile = filesForTest.FirstFile; - testMarkdown.HtmlFile = filesForTest.SecondFile; + sut.ChangelogFile = filesForTest.FirstFile; + sut.HtmlFile = filesForTest.SecondFile; // SUT - Assert.That(testMarkdown.Execute(), Is.True); + Assert.That(sut.Execute(), Is.True); string actualHtml = File.ReadAllText(filesForTest.SecondFile); Assert.That(actualHtml, Is.EqualTo(expectedHtml)); } } - [Test] - public void HtmlWithNoReleaseNotesElementIsCompletelyReplaced() + [TestCase(null)] + [TestCase("UTF-8")] + [TestCase("ISO-8859-1")] + public void HtmlWithNoReleaseNotesElement_IsCompletelyReplaced(string existingCharset) { - var testMarkdown = new CreateReleaseNotesHtml(); - using( - var filesForTest = new TwoTempFilesForTest(Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()+".Test.md"), - Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()+".Test.htm"))) + var sut = new CreateReleaseNotesHtml(); + using (var filesForTest = new TwoTempFilesForTest(GetRandomFileEndingWith(".Test.md"), + GetRandomFileEndingWith(".Test.htm"))) { - var ChangelogFile = filesForTest.FirstFile; + var changelogFile = filesForTest.FirstFile; var htmlFile = filesForTest.SecondFile; - File.WriteAllLines(ChangelogFile, - new[] - {"## 2.3.9", "* with some random content", "* does some things", "## 2.3.7", "* more", "## 2.2.2", "* things"}); - File.WriteAllLines(htmlFile, - new[] {"", "", "
", "", ""}); - testMarkdown.ChangelogFile = ChangelogFile; - testMarkdown.HtmlFile = htmlFile; - Assert.That(testMarkdown.Execute(), Is.True); - AssertThatXmlIn.File(htmlFile).HasNoMatchForXpath("//div[@notmarkdown]"); + File.WriteAllLines(changelogFile, new[] + { + "## 2.3.9", "* with some random content", "* does some things", "## 2.3.7", + "* more", "## 2.2.2", "* things" + }); + var htmlLines = new List(new[] + { "", "", NotReleaseNotesDiv, "", "" }); + if (existingCharset != null) + { + htmlLines.Insert(1, ""); + htmlLines.Insert(2, $""); + htmlLines.Insert(3, ""); + } + + File.WriteAllLines(htmlFile, htmlLines); + sut.ChangelogFile = changelogFile; + sut.HtmlFile = htmlFile; + Assert.That(sut.Execute(), Is.True); + AssertThatXmlIn.File(htmlFile).HasNoMatchForXpath(NotReleaseNotesDivXpath); + var expectedCharset = "UTF-8"; + AssertThatXmlIn.File(htmlFile).HasSpecifiedNumberOfMatchesForXpath($"/html/head/meta[@charset='{expectedCharset}']", 1); } } [Test] - public void HtmlWithReleaseNotesElementHasOnlyReleaseNoteElementChanged() + public void HtmlWithReleaseNotesElement_HasOnlyReleaseNoteElementChanged() { - var testMarkdown = new CreateReleaseNotesHtml(); - using( - var filesForTest = new TwoTempFilesForTest(Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()+".Test.md"), - Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()+".Test.htm"))) + var sut = new CreateReleaseNotesHtml(); + using (var filesForTest = new TwoTempFilesForTest(GetRandomFileEndingWith(".Test.md"), + GetRandomFileEndingWith(".Test.htm"))) { - var ChangelogFile = filesForTest.FirstFile; + var changelogFile = filesForTest.FirstFile; var htmlFile = filesForTest.SecondFile; - File.WriteAllLines(ChangelogFile, - new[] - {"## 2.3.9", "* with some random content", "* does some things", "## 2.3.7", "* more", "## 2.2.2", "* things"}); - File.WriteAllLines(htmlFile, - new[] {"", "", "
", "
", "", ""}); - testMarkdown.ChangelogFile = ChangelogFile; - testMarkdown.HtmlFile = htmlFile; - Assert.That(testMarkdown.Execute(), Is.True); - AssertThatXmlIn.File(htmlFile).HasSpecifiedNumberOfMatchesForXpath("//*[@class='notmarkdown']", 1); - AssertThatXmlIn.File(htmlFile).HasSpecifiedNumberOfMatchesForXpath("//*[@class='releasenotes']", 1); - AssertThatXmlIn.File(htmlFile).HasSpecifiedNumberOfMatchesForXpath("//*[@class='releasenotes']//*[text()[contains(., 'does some things')]]", 1); + File.WriteAllLines(changelogFile, new[] + { + "## 2.3.9", "* with some random content", "* does some things", "## 2.3.7", + "* more", "## 2.2.2", "* things" + }); + File.WriteAllLines(htmlFile, new[] + { + "", "", NotReleaseNotesDiv, + $"
", "", "" + }); + sut.ChangelogFile = changelogFile; + sut.HtmlFile = htmlFile; + Assert.That(sut.Execute(), Is.True); + AssertThatXmlIn.File(htmlFile).HasSpecifiedNumberOfMatchesForXpath( + NotReleaseNotesDivXpath, 1); + AssertThatXmlIn.File(htmlFile).HasSpecifiedNumberOfMatchesForXpath( + $"//*[@{ReleaseNotesClassAttribute}]", 1); + AssertThatXmlIn.File(htmlFile).HasSpecifiedNumberOfMatchesForXpath( + $"//*[@{ReleaseNotesClassAttribute}]//*[text()[contains(., 'does some things')]]", 1); } } [Test] - public void HtmlWithReleaseNotesElementWithContentsIsChanged() + public void HtmlWithReleaseNotesElementWithContents_IsChanged() { - var testMarkdown = new CreateReleaseNotesHtml(); - using( - var filesForTest = new TwoTempFilesForTest(Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()+".Test.md"), - Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()+".Test.htm"))) + var sut = new CreateReleaseNotesHtml(); + using (var filesForTest = new TwoTempFilesForTest(GetRandomFileEndingWith(".Test.md"), + GetRandomFileEndingWith(".Test.htm"))) { - var ChangelogFile = filesForTest.FirstFile; + var changelogFile = filesForTest.FirstFile; var htmlFile = filesForTest.SecondFile; - File.WriteAllLines(ChangelogFile, - new[] - {"## 2.3.9", "* with some random content", "* does some things", "## 2.3.7", "* more", "## 2.2.2", "* things"}); - File.WriteAllLines(htmlFile, - new[] + File.WriteAllLines(changelogFile, new[] + { + "## 2.3.9", "* with some random content", "* does some things", "## 2.3.7", + "* more", "## 2.2.2", "* things" + }); + File.WriteAllLines(htmlFile, new[] { - "", "", "
", "", "
", - "", "" + "", "", "", "", "", + $"
", + "", "
", "", "" }); - testMarkdown.ChangelogFile = ChangelogFile; - testMarkdown.HtmlFile = htmlFile; - Assert.That(testMarkdown.Execute(), Is.True); + sut.ChangelogFile = changelogFile; + sut.HtmlFile = htmlFile; + Assert.That(sut.Execute(), Is.True); + AssertThatXmlIn.File(htmlFile).HasSpecifiedNumberOfMatchesForXpath( + "/html/head/meta[@charset='UTF-8']", 1); AssertThatXmlIn.File(htmlFile).HasNoMatchForXpath("//span[@class='note']"); - AssertThatXmlIn.File(htmlFile).HasSpecifiedNumberOfMatchesForXpath("//*[@class='releasenotes']", 1); + AssertThatXmlIn.File(htmlFile).HasSpecifiedNumberOfMatchesForXpath( + $"//*[@{ReleaseNotesClassAttribute}]", 1); } } - } } diff --git a/SIL.ReleaseTasks/CreateReleaseNotesHtml.cs b/SIL.ReleaseTasks/CreateReleaseNotesHtml.cs index 32cded4a..910bbf41 100644 --- a/SIL.ReleaseTasks/CreateReleaseNotesHtml.cs +++ b/SIL.ReleaseTasks/CreateReleaseNotesHtml.cs @@ -1,4 +1,4 @@ -// Copyright (c) 2018 SIL Global +// Copyright (c) 2018-2025 SIL Global // This software is licensed under the MIT License (http://opensource.org/licenses/MIT) using System; @@ -15,13 +15,17 @@ namespace SIL.ReleaseTasks /// /// /// Given a markdown-style changelog file, this class will generate a release notes HTML file. - /// If the HTML file already exists, the task will look for a section with `class="releasenotes"` - /// and replace it with the current release notes. - /// The developer-oriented and [Unreleased] beginning will be removed from a Keep a Changelog style changelog. + /// If the HTML file already exists, the task will look for a section with + /// `class=">"` and replace it with the current release + /// notes. + /// The developer-oriented and [Unreleased] beginning will be removed from a Keep a Changelog + /// style changelog. /// [SuppressMessage("ReSharper", "MemberCanBePrivate.Global")] public class CreateReleaseNotesHtml : Task { + public const string kReleaseNotesClassName = "releasenotes"; + [Required] public string HtmlFile { get; set; } @@ -39,27 +43,28 @@ public override bool Execute() try { string inputMarkdown = File.ReadAllText(ChangelogFile); - CreateReleaseNotesHtml.RemoveKeepAChangelogHeadIfPresent(ref inputMarkdown); - // MarkdownDeep appears to use \n for newlines. Rather than mix those with platform line-endings, just - // convert them to platform line-endings if needed. + RemoveKeepAChangelogHeadIfPresent(ref inputMarkdown); + // MarkDig appears to use \n for newlines. Rather than mix those with platform + // line-endings, just convert them to platform line-endings if needed. var markdownHtml = Markdown.ToHtml(inputMarkdown).Replace("\n", Environment.NewLine); - if(File.Exists(HtmlFile)) + XElement releaseNotesElement = null; + XDocument htmlDoc = null; + if (File.Exists(HtmlFile)) + { + htmlDoc = XDocument.Load(HtmlFile); + var releaseNotesElementXpath = $"//*[@class='{kReleaseNotesClassName}']"; + releaseNotesElement = htmlDoc.XPathSelectElement(releaseNotesElementXpath); + } + if (releaseNotesElement == null) + WriteBasicHtmlFromMarkdown(markdownHtml); + else { - var htmlDoc = XDocument.Load(HtmlFile); - var releaseNotesElement = htmlDoc.XPathSelectElement("//*[@class='releasenotes']"); - if (releaseNotesElement == null) - return true; - releaseNotesElement.RemoveNodes(); var mdDocument = XDocument.Parse($"
{markdownHtml}
"); // ReSharper disable once PossibleNullReferenceException - Will either throw or work releaseNotesElement.Add(mdDocument.Root.Elements()); htmlDoc.Save(HtmlFile); } - else - { - WriteBasicHtmlFromMarkdown(markdownHtml); - } return true; } catch(Exception e) @@ -71,7 +76,9 @@ public override bool Execute() private void WriteBasicHtmlFromMarkdown(string markdownHtml) { - File.WriteAllText(HtmlFile, $"
{Environment.NewLine}{markdownHtml}
"); + File.WriteAllText(HtmlFile, "" + + $"
" + + $"{Environment.NewLine}{markdownHtml}
"); } /// @@ -81,9 +88,7 @@ private void WriteBasicHtmlFromMarkdown(string markdownHtml) public static bool RemoveKeepAChangelogHeadIfPresent(ref string md) { if (!md.Contains("[Unreleased]")) - { return false; - } string unreleasedHeader = $"## [Unreleased]{Environment.NewLine}{Environment.NewLine}"; int unreleasedHeaderLocation = md.IndexOf(unreleasedHeader); md = md.Substring(unreleasedHeaderLocation + unreleasedHeader.Length);