From 01422237dc72b017f3f648be0ba193aee7a2ddfb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9mie=20Bertrand?= Date: Sun, 22 Feb 2015 18:02:26 +0100 Subject: [PATCH] First version Fix #1 Fix #2 Fix #3 --- .../Properties/AssemblyInfo.cs | 36 ++ .../SemanticReleaseNotesFormatterTest.cs | 440 ++++++++++++++++++ ...manticReleaseNotesParser.Core.Tests.csproj | 81 ++++ .../SemanticReleaseNotesParserTest.cs | 401 ++++++++++++++++ .../packages.config | 6 + SemanticReleaseNotesParser.Core/Category.cs | 21 + .../Formatter/GroupBy.cs | 18 + .../Formatter/OutputFormat.cs | 18 + .../SemanticReleaseNotesFormatter.cs | 126 +++++ .../SemanticReleaseNotesFormatterSettings.cs | 30 ++ SemanticReleaseNotesParser.Core/Item.cs | 35 ++ .../Properties/AssemblyInfo.cs | 36 ++ .../ReleaseNotes.cs | 35 ++ .../Resources/GroupByCategories.liquid | 12 + .../Resources/GroupBySections.liquid | 12 + SemanticReleaseNotesParser.Core/Section.cs | 39 ++ .../SemanticReleaseNotesParser.Core.csproj | 76 +++ .../SemanticReleaseNotesParser.cs | 175 +++++++ .../packages.config | 5 + .../ArgumentsTest.cs | 187 ++++++++ .../BuildServers/AppVeyorTest.cs | 104 +++++ .../BuildServers/LocalBuildServerTest.cs | 60 +++ .../LoggerTest.cs | 86 ++++ .../ProgramTest.cs | 317 +++++++++++++ .../Properties/AssemblyInfo.cs | 36 ++ .../SemanticReleaseNotesParser.Tests.csproj | 97 ++++ .../packages.config | 9 + SemanticReleaseNotesParser.sln | 40 ++ .../Abstractions/EnvironmentWrapper.cs | 24 + .../Abstractions/IEnvironment.cs | 11 + .../Abstractions/IWebClient.cs | 9 + .../Abstractions/IWebClientFactory.cs | 7 + .../Abstractions/WebClientFactory.cs | 13 + .../Abstractions/WebClientWrapper.cs | 16 + SemanticReleaseNotesParser/Arguments.cs | 51 ++ .../BuildServers/AppVeyor.cs | 35 ++ .../BuildServers/IBuildServer.cs | 9 + .../BuildServers/LocalBuildServer.cs | 25 + SemanticReleaseNotesParser/Logger.cs | 57 +++ SemanticReleaseNotesParser/OutputType.cs | 8 + SemanticReleaseNotesParser/Program.cs | 133 ++++++ .../Properties/AssemblyInfo.cs | 38 ++ .../SemanticReleaseNotesParser.csproj | 106 +++++ SemanticReleaseNotesParser/packages.config | 7 + appveyor.yml | 40 ++ 45 files changed, 3127 insertions(+) create mode 100644 SemanticReleaseNotesParser.Core.Tests/Properties/AssemblyInfo.cs create mode 100644 SemanticReleaseNotesParser.Core.Tests/SemanticReleaseNotesFormatterTest.cs create mode 100644 SemanticReleaseNotesParser.Core.Tests/SemanticReleaseNotesParser.Core.Tests.csproj create mode 100644 SemanticReleaseNotesParser.Core.Tests/SemanticReleaseNotesParserTest.cs create mode 100644 SemanticReleaseNotesParser.Core.Tests/packages.config create mode 100644 SemanticReleaseNotesParser.Core/Category.cs create mode 100644 SemanticReleaseNotesParser.Core/Formatter/GroupBy.cs create mode 100644 SemanticReleaseNotesParser.Core/Formatter/OutputFormat.cs create mode 100644 SemanticReleaseNotesParser.Core/Formatter/SemanticReleaseNotesFormatter.cs create mode 100644 SemanticReleaseNotesParser.Core/Formatter/SemanticReleaseNotesFormatterSettings.cs create mode 100644 SemanticReleaseNotesParser.Core/Item.cs create mode 100644 SemanticReleaseNotesParser.Core/Properties/AssemblyInfo.cs create mode 100644 SemanticReleaseNotesParser.Core/ReleaseNotes.cs create mode 100644 SemanticReleaseNotesParser.Core/Resources/GroupByCategories.liquid create mode 100644 SemanticReleaseNotesParser.Core/Resources/GroupBySections.liquid create mode 100644 SemanticReleaseNotesParser.Core/Section.cs create mode 100644 SemanticReleaseNotesParser.Core/SemanticReleaseNotesParser.Core.csproj create mode 100644 SemanticReleaseNotesParser.Core/SemanticReleaseNotesParser.cs create mode 100644 SemanticReleaseNotesParser.Core/packages.config create mode 100644 SemanticReleaseNotesParser.Tests/ArgumentsTest.cs create mode 100644 SemanticReleaseNotesParser.Tests/BuildServers/AppVeyorTest.cs create mode 100644 SemanticReleaseNotesParser.Tests/BuildServers/LocalBuildServerTest.cs create mode 100644 SemanticReleaseNotesParser.Tests/LoggerTest.cs create mode 100644 SemanticReleaseNotesParser.Tests/ProgramTest.cs create mode 100644 SemanticReleaseNotesParser.Tests/Properties/AssemblyInfo.cs create mode 100644 SemanticReleaseNotesParser.Tests/SemanticReleaseNotesParser.Tests.csproj create mode 100644 SemanticReleaseNotesParser.Tests/packages.config create mode 100644 SemanticReleaseNotesParser.sln create mode 100644 SemanticReleaseNotesParser/Abstractions/EnvironmentWrapper.cs create mode 100644 SemanticReleaseNotesParser/Abstractions/IEnvironment.cs create mode 100644 SemanticReleaseNotesParser/Abstractions/IWebClient.cs create mode 100644 SemanticReleaseNotesParser/Abstractions/IWebClientFactory.cs create mode 100644 SemanticReleaseNotesParser/Abstractions/WebClientFactory.cs create mode 100644 SemanticReleaseNotesParser/Abstractions/WebClientWrapper.cs create mode 100644 SemanticReleaseNotesParser/Arguments.cs create mode 100644 SemanticReleaseNotesParser/BuildServers/AppVeyor.cs create mode 100644 SemanticReleaseNotesParser/BuildServers/IBuildServer.cs create mode 100644 SemanticReleaseNotesParser/BuildServers/LocalBuildServer.cs create mode 100644 SemanticReleaseNotesParser/Logger.cs create mode 100644 SemanticReleaseNotesParser/OutputType.cs create mode 100644 SemanticReleaseNotesParser/Program.cs create mode 100644 SemanticReleaseNotesParser/Properties/AssemblyInfo.cs create mode 100644 SemanticReleaseNotesParser/SemanticReleaseNotesParser.csproj create mode 100644 SemanticReleaseNotesParser/packages.config create mode 100644 appveyor.yml diff --git a/SemanticReleaseNotesParser.Core.Tests/Properties/AssemblyInfo.cs b/SemanticReleaseNotesParser.Core.Tests/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..75ff4cc --- /dev/null +++ b/SemanticReleaseNotesParser.Core.Tests/Properties/AssemblyInfo.cs @@ -0,0 +1,36 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("SemanticReleaseNotesParser.Core.Tests")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("SemanticReleaseNotesParser.Core.Tests")] +[assembly: AssemblyCopyright("Copyright © 2015")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("e88ee3ce-bdbf-418b-8584-066765aebe4b")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/SemanticReleaseNotesParser.Core.Tests/SemanticReleaseNotesFormatterTest.cs b/SemanticReleaseNotesParser.Core.Tests/SemanticReleaseNotesFormatterTest.cs new file mode 100644 index 0000000..d597a11 --- /dev/null +++ b/SemanticReleaseNotesParser.Core.Tests/SemanticReleaseNotesFormatterTest.cs @@ -0,0 +1,440 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Text; +using Xunit; + +namespace SemanticReleaseNotesParser.Core.Tests +{ + public class SemanticReleaseNotesFormatterTest + { + [Fact] + public void Format_Null_Exception() + { + // act & assert + var exception = Assert.Throws(() => SemanticReleaseNotesFormatter.Format(null)); + Assert.Equal("releaseNotes", exception.ParamName); + } + + [Fact] + public void Format_TextWriter_Null_Exception() + { + // act & assert + var exception = Assert.Throws(() => SemanticReleaseNotesFormatter.Format(null, new ReleaseNotes())); + Assert.Equal("writer", exception.ParamName); + } + + [Fact] + public void Format_ExampleA_Default() + { + // act + var resultHtml = SemanticReleaseNotesFormatter.Format(GetExempleAReleaseNotes()); + + // assert + Assert.Equal(ExampleAHtml, resultHtml.Trim()); + } + + [Fact] + public void Format_TextWriter_ExampleA_Default() + { + // arrange + var resultHtml = new StringBuilder(); + + // act + SemanticReleaseNotesFormatter.Format(new StringWriter(resultHtml), GetExempleAReleaseNotes()); + + // assert + Assert.Equal(ExampleAHtml, resultHtml.ToString().Trim()); + } + + [Fact] + public void Format_ExampleA_Output_Html() + { + // act + var resultHtml = SemanticReleaseNotesFormatter.Format(GetExempleAReleaseNotes(), new SemanticReleaseNotesFormatterSettings { OutputFormat = OutputFormat.Html }); + + // assert + Assert.Equal(ExampleAHtml, resultHtml.Trim()); + } + + [Fact] + public void Format_ExampleA_Output_Markdown() + { + // act + var resultHtml = SemanticReleaseNotesFormatter.Format(GetExempleAReleaseNotes(), new SemanticReleaseNotesFormatterSettings { OutputFormat = OutputFormat.Markdown }); + + // assert + Assert.Equal(ExampleAMarkdown, resultHtml.Trim()); + } + + [Fact] + public void Format_ExampleA_Output_Html_GroupBy_Categories() + { + // act + var resultHtml = SemanticReleaseNotesFormatter.Format(GetExempleAReleaseNotes(), new SemanticReleaseNotesFormatterSettings { OutputFormat = OutputFormat.Html, GroupBy = GroupBy.Categories }); + + // assert + Assert.Equal(ExampleAHtmlCategories, resultHtml.Trim()); + } + + [Fact] + public void Format_ExampleA_Output_Markdown_GroupBy_Categories() + { + // act + var resultHtml = SemanticReleaseNotesFormatter.Format(GetExempleAReleaseNotes(), new SemanticReleaseNotesFormatterSettings { OutputFormat = OutputFormat.Markdown, GroupBy = GroupBy.Categories }); + + // assert + Assert.Equal(ExampleAMarkdownCategories, resultHtml.Trim()); + } + + [Fact] + public void Format_ExampleB_Output_Html() + { + // act + var resultHtml = SemanticReleaseNotesFormatter.Format(GetExempleBReleaseNotes(), new SemanticReleaseNotesFormatterSettings { OutputFormat = OutputFormat.Html }); + + // assert + Assert.Equal(ExampleBHtml, resultHtml.Trim()); + } + + [Fact] + public void Format_ExampleB_Output_Markdown() + { + // act + var resultHtml = SemanticReleaseNotesFormatter.Format(GetExempleBReleaseNotes(), new SemanticReleaseNotesFormatterSettings { OutputFormat = OutputFormat.Markdown }); + + // assert + Assert.Equal(ExampleBMarkdown, resultHtml.Trim()); + } + + [Fact] + public void Format_ExampleA_Output_Html_Custom_LiquidTemplate() + { + // act + var resultHtml = SemanticReleaseNotesFormatter.Format(GetExempleAReleaseNotes(), new SemanticReleaseNotesFormatterSettings { OutputFormat = OutputFormat.Html, LiquidTemplate = CustomLiquidTemplate }); + + // assert + Assert.Equal(CustomLiquidTemplateHtml, resultHtml.Trim()); + } + + [Fact] + public void Format_ExampleA_Output_Markdown_Custom_LiquidTemplate() + { + // act + var resultHtml = SemanticReleaseNotesFormatter.Format(GetExempleAReleaseNotes(), new SemanticReleaseNotesFormatterSettings { OutputFormat = OutputFormat.Markdown, LiquidTemplate = CustomLiquidTemplate }); + + // assert + Assert.Equal(CustomLiquidTemplateMarkdown, resultHtml.Trim()); + } + + [Fact] + public void Format_ExampleC_Output_Html() + { + // act + var resultHtml = SemanticReleaseNotesFormatter.Format(GetExempleCReleaseNotes(), new SemanticReleaseNotesFormatterSettings { OutputFormat = OutputFormat.Html }); + + // assert + Assert.Equal(ExampleCHtml, resultHtml.Trim()); + } + + [Fact] + public void Format_ExampleC_Output_Markdown() + { + // act + var resultHtml = SemanticReleaseNotesFormatter.Format(GetExempleCReleaseNotes(), new SemanticReleaseNotesFormatterSettings { OutputFormat = OutputFormat.Markdown }); + + // assert + Assert.Equal(ExampleCMarkdown, resultHtml.Trim()); + } + + [Fact] + public void Format_ExampleC_Output_Html_GroupBy_Categories() + { + // act + var resultHtml = SemanticReleaseNotesFormatter.Format(GetExempleCReleaseNotes(), new SemanticReleaseNotesFormatterSettings { OutputFormat = OutputFormat.Html, GroupBy = GroupBy.Categories }); + + // assert + Assert.Equal(ExampleCHtmlCategories, resultHtml.Trim()); + } + + [Fact] + public void Format_ExampleC_Output_Markdown_GroupBy_Categories() + { + // act + var resultHtml = SemanticReleaseNotesFormatter.Format(GetExempleCReleaseNotes(), new SemanticReleaseNotesFormatterSettings { OutputFormat = OutputFormat.Markdown, GroupBy = GroupBy.Categories }); + + // assert + Assert.Equal(ExampleCMarkdownCategories, resultHtml.Trim()); + } + + [Fact] + public void Format_ExampleD_Output_Html() + { + // act + var exception = Assert.Throws(() => SemanticReleaseNotesFormatter.Format(GetExempleDReleaseNotes(), new SemanticReleaseNotesFormatterSettings { OutputFormat = OutputFormat.Html })); + + // assert + Assert.Equal("The priorities for items are not supported currently for Html output.", exception.Message); + //Assert.Equal(ExampleDHtml, resultHtml.Trim()); + } + + [Fact] + public void Format_ExampleD_Output_Markdown() + { + // act + var resultHtml = SemanticReleaseNotesFormatter.Format(GetExempleDReleaseNotes(), new SemanticReleaseNotesFormatterSettings { OutputFormat = OutputFormat.Markdown }); + + // assert + Assert.Equal(ExampleDMarkdown, resultHtml.Trim()); + } + + private ReleaseNotes GetExempleAReleaseNotes() + { + return new ReleaseNotes + { + Summary = "Incremental release designed to provide an update to some of the core plugins.", + Items = new List + { + new Item { Category = "New", Summary = "Release Checker: Now gives you a breakdown of exactly what you are missing." }, + new Item { Category = "New", Summary = "Structured Layout: An alternative layout engine that allows developers to control layout." }, + new Item { Category = "Changed", Summary = "Timeline: Comes with an additional grid view to show the same data." }, + new Item { Category = "Fix", Summary = "Ajax: Fix that crashed poll in Chrome and IE due to log/trace statement." } + } + }; + } + + private ReleaseNotes GetExempleBReleaseNotes() + { + return new ReleaseNotes + { + Summary = "Incremental release designed to provide an update to some of the core plugins.", + Sections = new List
+ { + new Section { Name = "System", Items = new List + { + new Item { Category = "New", Summary = "*Release Checker*: Now gives you a breakdown of exactly what you are missing." }, + new Item { Category = "New", Summary = "*Structured Layout*: An alternative layout engine that allows developers to control layout." } + } }, + new Section { Name = "Plugin", Items = new List + { + new Item { Category = "Changed", Summary = "*Timeline*: Comes with an additional grid view to show the same data." }, + new Item { Category = "Fix", Summary = "*Ajax*: Fix that crashed poll in Chrome and IE due to log/trace statement." } + } } + } + }; + } + + private ReleaseNotes GetExempleCReleaseNotes() + { + return new ReleaseNotes + { + Summary = "Incremental release designed to provide an update to some of the core plugins.", + Items = new List { new Item { Summary = "*Example*: You can have global issues that aren't grouped to a section" } }, + Sections = new List
+ { + new Section { Name = "System", Summary = "This description is specific to system section.", Items = new List + { + new Item { Category = "new", Summary = "*Release Checker*: Now gives you a breakdown of exactly what you are missing." }, + new Item { Category = "new", Summary = "*Structured Layout*: An alternative layout engine that allows developers to control layout." } + } }, + new Section { Name = "Plugin", Summary = "This description is specific to plugin section.", Items = new List + { + new Item { Category = "Changed", Summary = "*Timeline*: Comes with an additional grid view to show the same data." }, + new Item { Category = "Fix", Summary = "*Ajax*: Fix that crashed poll in Chrome and IE due to log/trace statement.", TaskId = "i1234", TaskLink = "http://getglimpse.com" } + } } + } + }; + } + + private ReleaseNotes GetExempleDReleaseNotes() + { + return new ReleaseNotes + { + Summary = "Incremental release designed to provide an update to some of the core plugins.", + Items = new List { new Item { Priority = 1, Summary = "*Example*: You can have global issues that aren't grouped to a section" } }, + Sections = new List
+ { + new Section { Name = "System", Summary = "This description is specific to system section.", Icon = "http://getglimpse.com/release/icon/core.png", Items = new List + { + new Item { Priority = 3, Category = "new", Summary = "*Release Checker*: Now gives you a breakdown of exactly what you are missing." }, + new Item { Priority = 2, Category = "new", Summary = "*Structured Layout*: An alternative layout engine that allows developers to control layout." } + } }, + new Section { Name = "Plugin", Summary = "This description is specific to plugin section.", Icon= "http://getglimpse.com/release/icon/mvc.png", Items = new List + { + new Item { Priority = 1, Category = "Changed", Summary = "*Timeline*: Comes with an additional grid view to show the same data." }, + new Item { Priority = 1, Category = "Fix", Summary = "*Ajax*: Fix that crashed poll in Chrome and IE due to log/trace statement.", TaskId = "i1234", TaskLink = "http://getglimpse.com" } + } } + } + }; + } + + private const string ExampleAHtml = @"

Incremental release designed to provide an update to some of the core plugins.

+
    +
  • {New} Release Checker: Now gives you a breakdown of exactly what you are missing.
  • +
  • {New} Structured Layout: An alternative layout engine that allows developers to control layout.
  • +
  • {Changed} Timeline: Comes with an additional grid view to show the same data.
  • +
  • {Fix} Ajax: Fix that crashed poll in Chrome and IE due to log/trace statement.
  • +
"; + + private const string ExampleAMarkdown = @"Incremental release designed to provide an update to some of the core plugins. + - {New} Release Checker: Now gives you a breakdown of exactly what you are missing. + - {New} Structured Layout: An alternative layout engine that allows developers to control layout. + - {Changed} Timeline: Comes with an additional grid view to show the same data. + - {Fix} Ajax: Fix that crashed poll in Chrome and IE due to log/trace statement."; + + private const string ExampleAHtmlCategories = @"

Incremental release designed to provide an update to some of the core plugins.

+

New

+
    +
  • Release Checker: Now gives you a breakdown of exactly what you are missing.
  • +
  • Structured Layout: An alternative layout engine that allows developers to control layout.
  • +
+

Changed

+
    +
  • Timeline: Comes with an additional grid view to show the same data.
  • +
+

Fix

+
    +
  • Ajax: Fix that crashed poll in Chrome and IE due to log/trace statement.
  • +
"; + + private const string ExampleAMarkdownCategories = @"Incremental release designed to provide an update to some of the core plugins. +# New + - Release Checker: Now gives you a breakdown of exactly what you are missing. + - Structured Layout: An alternative layout engine that allows developers to control layout. +# Changed + - Timeline: Comes with an additional grid view to show the same data. +# Fix + - Ajax: Fix that crashed poll in Chrome and IE due to log/trace statement."; + + private const string ExampleBHtml = @"

Incremental release designed to provide an update to some of the core plugins.

+

System

+
    +
  • {New} Release Checker: Now gives you a breakdown of exactly what you are missing.
  • +
  • {New} Structured Layout: An alternative layout engine that allows developers to control layout.
  • +
+

Plugin

+
    +
  • {Changed} Timeline: Comes with an additional grid view to show the same data.
  • +
  • {Fix} Ajax: Fix that crashed poll in Chrome and IE due to log/trace statement.
  • +
"; + + private const string ExampleBMarkdown = @"Incremental release designed to provide an update to some of the core plugins. +# System + - {New} *Release Checker*: Now gives you a breakdown of exactly what you are missing. + - {New} *Structured Layout*: An alternative layout engine that allows developers to control layout. +# Plugin + - {Changed} *Timeline*: Comes with an additional grid view to show the same data. + - {Fix} *Ajax*: Fix that crashed poll in Chrome and IE due to log/trace statement."; + + private const string CustomLiquidTemplate = @" +{%- for category in categories -%} +# {{ category.name }} +{%- for item in category.items -%} + - {{ item.summary }} +{%- endfor -%} +{%- endfor -%} + +{{ release_notes.summary }}"; + + private const string CustomLiquidTemplateHtml = @"

New

+
    +
  • Release Checker: Now gives you a breakdown of exactly what you are missing.
  • +
  • Structured Layout: An alternative layout engine that allows developers to control layout.
  • +
+

Changed

+
    +
  • Timeline: Comes with an additional grid view to show the same data.
  • +
+

Fix

+
    +
  • Ajax: Fix that crashed poll in Chrome and IE due to log/trace statement.
  • +
+

Incremental release designed to provide an update to some of the core plugins.

"; + + private const string CustomLiquidTemplateMarkdown = @"# New + - Release Checker: Now gives you a breakdown of exactly what you are missing. + - Structured Layout: An alternative layout engine that allows developers to control layout. +# Changed + - Timeline: Comes with an additional grid view to show the same data. +# Fix + - Ajax: Fix that crashed poll in Chrome and IE due to log/trace statement. + +Incremental release designed to provide an update to some of the core plugins."; + + private const string ExampleCHtml = @"

Incremental release designed to provide an update to some of the core plugins.

+
    +
  • Example: You can have global issues that aren't grouped to a section
  • +
+

System

+
    +
  • {new} Release Checker: Now gives you a breakdown of exactly what you are missing.
  • +
  • {new} Structured Layout: An alternative layout engine that allows developers to control layout.
  • +
+

Plugin

+
    +
  • {Changed} Timeline: Comes with an additional grid view to show the same data.
  • +
  • {Fix} Ajax: Fix that crashed poll in Chrome and IE due to log/trace statement. i1234
  • +
"; + + private const string ExampleCMarkdown = @"Incremental release designed to provide an update to some of the core plugins. + - *Example*: You can have global issues that aren't grouped to a section +# System + - {new} *Release Checker*: Now gives you a breakdown of exactly what you are missing. + - {new} *Structured Layout*: An alternative layout engine that allows developers to control layout. +# Plugin + - {Changed} *Timeline*: Comes with an additional grid view to show the same data. + - {Fix} *Ajax*: Fix that crashed poll in Chrome and IE due to log/trace statement. [i1234](http://getglimpse.com)"; + + private const string ExampleCHtmlCategories = @"

Incremental release designed to provide an update to some of the core plugins.

+
    +
  • Example: You can have global issues that aren't grouped to a section
  • +
+

New

+
    +
  • Release Checker: Now gives you a breakdown of exactly what you are missing.
  • +
  • Structured Layout: An alternative layout engine that allows developers to control layout.
  • +
+

Changed

+
    +
  • Timeline: Comes with an additional grid view to show the same data.
  • +
+

Fix

+
    +
  • Ajax: Fix that crashed poll in Chrome and IE due to log/trace statement. i1234
  • +
"; + + private const string ExampleCMarkdownCategories = @"Incremental release designed to provide an update to some of the core plugins. + - *Example*: You can have global issues that aren't grouped to a section +# New + - *Release Checker*: Now gives you a breakdown of exactly what you are missing. + - *Structured Layout*: An alternative layout engine that allows developers to control layout. +# Changed + - *Timeline*: Comes with an additional grid view to show the same data. +# Fix + - *Ajax*: Fix that crashed poll in Chrome and IE due to log/trace statement. [i1234](http://getglimpse.com)"; + + private const string ExampleDHtml = @"

Incremental release designed to provide an update to some of the core plugins.

+
    +
  • Example: You can have global issues that aren't grouped to a section
  • +
+

System

+
    +
  • {new} Release Checker: Now gives you a breakdown of exactly what you are missing.
  • +
  • {new} Structured Layout: An alternative layout engine that allows developers to control layout.
  • +
+

Plugin

+
    +
  • {Changed} Timeline: Comes with an additional grid view to show the same data.
  • +
  • {Fix} Ajax: Fix that crashed poll in Chrome and IE due to log/trace statement. i1234
  • +
"; + + private const string ExampleDMarkdown = @"Incremental release designed to provide an update to some of the core plugins. + 1. *Example*: You can have global issues that aren't grouped to a section +# System + 3. {new} *Release Checker*: Now gives you a breakdown of exactly what you are missing. + 2. {new} *Structured Layout*: An alternative layout engine that allows developers to control layout. +# Plugin + 1. {Changed} *Timeline*: Comes with an additional grid view to show the same data. + 1. {Fix} *Ajax*: Fix that crashed poll in Chrome and IE due to log/trace statement. [i1234](http://getglimpse.com)"; + } +} diff --git a/SemanticReleaseNotesParser.Core.Tests/SemanticReleaseNotesParser.Core.Tests.csproj b/SemanticReleaseNotesParser.Core.Tests/SemanticReleaseNotesParser.Core.Tests.csproj new file mode 100644 index 0000000..f77b18b --- /dev/null +++ b/SemanticReleaseNotesParser.Core.Tests/SemanticReleaseNotesParser.Core.Tests.csproj @@ -0,0 +1,81 @@ + + + + + + Debug + AnyCPU + {90B46F40-8589-4665-AFF9-4CA01FAF03AD} + Library + Properties + SemanticReleaseNotesParser.Core.Tests + SemanticReleaseNotesParser.Core.Tests + v4.0 + 512 + e6b1ab81 + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + False + ..\packages\DotLiquid.1.8.0\lib\NET40\DotLiquid.dll + + + + + + + + + + ..\packages\xunit.1.9.2\lib\net20\xunit.dll + + + + + + + + + + + + + {982453c5-147d-435e-b70a-e25e6c2ee748} + SemanticReleaseNotesParser.Core + + + + + + + + + This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. + + + + + \ No newline at end of file diff --git a/SemanticReleaseNotesParser.Core.Tests/SemanticReleaseNotesParserTest.cs b/SemanticReleaseNotesParser.Core.Tests/SemanticReleaseNotesParserTest.cs new file mode 100644 index 0000000..d8c4c4c --- /dev/null +++ b/SemanticReleaseNotesParser.Core.Tests/SemanticReleaseNotesParserTest.cs @@ -0,0 +1,401 @@ +using System; +using System.IO; +using Xunit; + +namespace SemanticReleaseNotesParser.Core.Tests +{ + public class SemanticReleaseNotesParserTest + { + [Fact] + public void Parse_Null_Exception() + { + // act & assert + var exception = Assert.Throws(() => SemanticReleaseNotesParser.Parse((string)null)); + Assert.Equal("rawReleaseNotes", exception.ParamName); + } + + [Fact] + public void Parse_TextReader_Null_Exception() + { + // act & assert + var exception = Assert.Throws(() => SemanticReleaseNotesParser.Parse((TextReader)null)); + Assert.Equal("reader", exception.ParamName); + } + + [Fact] + public void Parse_Syntax_Summaries() + { + // act + var releaseNote = SemanticReleaseNotesParser.Parse(Syntax_Summaries); + + // assert + Assert.Equal("This is a _project_ summary with two paragraphs.Lorem ipsum dolor sit amet consectetuer **adipiscing** elit.Aliquam hendreritmi posuere lectus.\r\n\r\nVestibulum `enim wisi` viverra nec fringilla in laoreetvitae risus. Donec sit amet nisl. Aliquam [semper](?) ipsumsit amet velit.", releaseNote.Summary); + } + + [Fact] + public void Parse_TextReader_Syntax_Summaries() + { + // act + var releaseNote = SemanticReleaseNotesParser.Parse(GetTextReader(Syntax_Summaries)); + + // assert + Assert.Equal("This is a _project_ summary with two paragraphs.Lorem ipsum dolor sit amet consectetuer **adipiscing** elit.Aliquam hendreritmi posuere lectus.\r\n\r\nVestibulum `enim wisi` viverra nec fringilla in laoreetvitae risus. Donec sit amet nisl. Aliquam [semper](?) ipsumsit amet velit.", releaseNote.Summary); + } + + [Fact] + public void Parse_Syntax_Items() + { + // act + var releaseNote = SemanticReleaseNotesParser.Parse(Syntax_Items); + + // assert + Assert.Equal(4, releaseNote.Items.Count); + + Assert.Equal("This is the _first_ *list* item.", releaseNote.Items[0].Summary); + Assert.Equal("This is the **second** __list__ item.", releaseNote.Items[1].Summary); + Assert.Equal("This is the `third` list item.", releaseNote.Items[2].Summary); + Assert.Equal("This is the [forth](?) list item.", releaseNote.Items[3].Summary); + } + + [Fact] + public void Parse_Syntax_Sections() + { + // act + var releaseNote = SemanticReleaseNotesParser.Parse(Syntax_Sections); + + // assert + Assert.Equal(2, releaseNote.Sections.Count); + + Assert.Equal("Section", releaseNote.Sections[0].Name); + Assert.Equal("This is the summary for Section.", releaseNote.Sections[0].Summary); + + Assert.Equal(2, releaseNote.Sections[0].Items.Count); + Assert.Equal("This is a Section scoped first list item.", releaseNote.Sections[0].Items[0].Summary); + Assert.Equal("This is a Section scoped second list item.", releaseNote.Sections[0].Items[1].Summary); + + Assert.Equal("Other Section", releaseNote.Sections[1].Name); + Assert.Equal("This is the summary for Other Section.", releaseNote.Sections[1].Summary); + + Assert.Equal(2, releaseNote.Sections[1].Items.Count); + Assert.Equal("This is a Other Section scoped first list item.", releaseNote.Sections[1].Items[0].Summary); + Assert.Equal("This is a Other Section scoped second list item.", releaseNote.Sections[1].Items[1].Summary); + } + + [Fact] + public void Parse_Syntax_Priority() + { + // act + var releaseNote = SemanticReleaseNotesParser.Parse(Syntax_Priority); + + // assert + Assert.Equal(string.Empty, releaseNote.Summary); + Assert.Equal(7, releaseNote.Items.Count); + + Assert.Equal(1, releaseNote.Items[0].Priority); + Assert.Equal("This is a High priority list item.", releaseNote.Items[0].Summary); + + Assert.Equal(1, releaseNote.Items[1].Priority); + Assert.Equal("This is a High priority list item.", releaseNote.Items[1].Summary); + + Assert.Equal(2, releaseNote.Items[2].Priority); + Assert.Equal("This is a Normal priority list item.", releaseNote.Items[2].Summary); + + Assert.Equal(1, releaseNote.Items[3].Priority); + Assert.Equal("This is a High priority list item.", releaseNote.Items[3].Summary); + + Assert.Equal(2, releaseNote.Items[4].Priority); + Assert.Equal("This is a Normal priority list item.", releaseNote.Items[4].Summary); + + Assert.Equal(3, releaseNote.Items[5].Priority); + Assert.Equal("This is a Minor priority list item.", releaseNote.Items[5].Summary); + + Assert.Equal(3, releaseNote.Items[6].Priority); + Assert.Equal("This is a Minor priority list item.", releaseNote.Items[6].Summary); + } + + [Fact] + public void Parse_Syntax_Category() + { + // act + var releaseNote = SemanticReleaseNotesParser.Parse(Syntax_Category); + + // assert + Assert.Equal(string.Empty, releaseNote.Summary); + Assert.Equal(7, releaseNote.Items.Count); + + Assert.Equal("New", releaseNote.Items[0].Category); + Assert.Equal("This is a list item.", releaseNote.Items[0].Summary); + + Assert.Equal("Fix", releaseNote.Items[1].Category); + Assert.Equal("This is a list item.", releaseNote.Items[1].Summary); + + Assert.Equal("Change", releaseNote.Items[2].Category); + Assert.Equal("This is a list item.", releaseNote.Items[2].Summary); + + Assert.Equal("New", releaseNote.Items[3].Category); + Assert.Equal("features are everyone's favorites.", releaseNote.Items[3].Summary); + + Assert.Equal("Developer.", releaseNote.Items[4].Category); + Assert.Equal("This is a list item for a", releaseNote.Items[4].Summary); + + Assert.Equal("Super-Special", releaseNote.Items[5].Category); + Assert.Equal("This is a custom list item.", releaseNote.Items[5].Summary); + + Assert.Equal("o", releaseNote.Items[6].Category); + Assert.Equal("This is a ne letter category.", releaseNote.Items[6].Summary); + } + + [Fact] + public void Parse_Example_A() + { + // act + var releaseNote = SemanticReleaseNotesParser.Parse(ExampleA); + + // assert + Assert.Equal("Incremental release designed to provide an update to some of the core plugins.", releaseNote.Summary); + Assert.Equal(4, releaseNote.Items.Count); + Assert.Equal("Release Checker: Now gives you a breakdown of exactly what you are missing.", releaseNote.Items[0].Summary); + Assert.Equal("New", releaseNote.Items[0].Category); + + Assert.Equal("Structured Layout: An alternative layout engine that allows developers to control layout.", releaseNote.Items[1].Summary); + Assert.Equal("New", releaseNote.Items[1].Category); + + Assert.Equal("Timeline: Comes with an additional grid view to show the same data.", releaseNote.Items[2].Summary); + Assert.Equal("Changed", releaseNote.Items[2].Category); + + Assert.Equal("Ajax: Fix that crashed poll in Chrome and IE due to log/trace statement.", releaseNote.Items[3].Summary); + Assert.Equal("Fix", releaseNote.Items[3].Category); + } + + [Fact] + public void Parse_Example_B() + { + // act + var releaseNote = SemanticReleaseNotesParser.Parse(ExampleB); + + // assert + Assert.Equal("Incremental release designed to provide an update to some of the core plugins.", releaseNote.Summary); + Assert.Equal(0, releaseNote.Items.Count); + + Assert.Equal(2, releaseNote.Sections.Count); + + Assert.Equal("System", releaseNote.Sections[0].Name); + Assert.Equal(2, releaseNote.Sections[0].Items.Count); + + Assert.Equal("*Release Checker*: Now gives you a breakdown of exactly what you are missing.", releaseNote.Sections[0].Items[0].Summary); + Assert.Equal("New", releaseNote.Sections[0].Items[0].Category); + + Assert.Equal("*Structured Layout*: An alternative layout engine that allows developers to control layout.", releaseNote.Sections[0].Items[1].Summary); + Assert.Equal("New", releaseNote.Sections[0].Items[1].Category); + + Assert.Equal("Plugin", releaseNote.Sections[1].Name); + Assert.Equal(2, releaseNote.Sections[1].Items.Count); + + Assert.Equal("*Timeline*: Comes with an additional grid view to show the same data.", releaseNote.Sections[1].Items[0].Summary); + Assert.Equal("Changed", releaseNote.Sections[1].Items[0].Category); + + Assert.Equal("*Ajax*: Fix that crashed poll in Chrome and IE due to log/trace statement.", releaseNote.Sections[1].Items[1].Summary); + Assert.Equal("Fix", releaseNote.Sections[1].Items[1].Category); + } + + [Fact] + public void Parse_Example_C() + { + // act + var releaseNote = SemanticReleaseNotesParser.Parse(ExampleC); + + // assert + Assert.Equal("Incremental release designed to provide an update to some of the core plugins.", releaseNote.Summary); + + Assert.Equal(1, releaseNote.Items.Count); + Assert.Equal("*Example*: You can have global issues that aren't grouped to a section", releaseNote.Items[0].Summary); + + Assert.Equal(2, releaseNote.Sections.Count); + + Assert.Equal("System", releaseNote.Sections[0].Name); + Assert.Equal("This description is specific to system section.", releaseNote.Sections[0].Summary); + Assert.Equal(2, releaseNote.Sections[0].Items.Count); + + Assert.Equal("*Release Checker*: Now gives you a breakdown of exactly what you are missing.", releaseNote.Sections[0].Items[0].Summary); + Assert.Equal("new", releaseNote.Sections[0].Items[0].Category); + + Assert.Equal("*Structured Layout*: A alternative layout engine that allows developers to control layout.", releaseNote.Sections[0].Items[1].Summary); + Assert.Equal("new", releaseNote.Sections[0].Items[1].Category); + + Assert.Equal("Plugin", releaseNote.Sections[1].Name); + Assert.Equal("This description is specific to plugin section.", releaseNote.Sections[1].Summary); + Assert.Equal(2, releaseNote.Sections[1].Items.Count); + + Assert.Equal("*Timeline*: Comes with an additional grid view to show the same data.", releaseNote.Sections[1].Items[0].Summary); + Assert.Equal("Changed", releaseNote.Sections[1].Items[0].Category); + + Assert.Equal("*Ajax*: that crashed poll in Chrome and IE due to log/trace statement.", releaseNote.Sections[1].Items[1].Summary); + Assert.Equal("Fix", releaseNote.Sections[1].Items[1].Category); + Assert.Equal("i1234", releaseNote.Sections[1].Items[1].TaskId); + Assert.Equal("http://getglimpse.com", releaseNote.Sections[1].Items[1].TaskLink); + } + + [Fact] + public void Parse_Example_D() + { + // act + var releaseNote = SemanticReleaseNotesParser.Parse(ExampleD); + + // assert + Assert.Equal("Incremental release designed to provide an update to some of the core plugins.", releaseNote.Summary); + + Assert.Equal(1, releaseNote.Items.Count); + Assert.Equal(1, releaseNote.Items[0].Priority); + Assert.Equal("*Example*: You can have global issues that aren't grouped to a section", releaseNote.Items[0].Summary); + + Assert.Equal(2, releaseNote.Sections.Count); + + Assert.Equal("System ", releaseNote.Sections[0].Name); + Assert.Equal("This description is specific to system section.", releaseNote.Sections[0].Summary); + Assert.Equal("http://getglimpse.com/release/icon/core.png", releaseNote.Sections[0].Icon); + Assert.Equal(2, releaseNote.Sections[0].Items.Count); + + Assert.Equal("*Release Checker*: Now gives you a breakdown of exactly what you are missing.", releaseNote.Sections[0].Items[0].Summary); + Assert.Equal("New", releaseNote.Sections[0].Items[0].Category); + Assert.Equal(3, releaseNote.Sections[0].Items[0].Priority); + + Assert.Equal("*Structured Layout*: An alternative layout engine that allows developers to control layout.", releaseNote.Sections[0].Items[1].Summary); + Assert.Equal("New", releaseNote.Sections[0].Items[1].Category); + Assert.Equal(2, releaseNote.Sections[0].Items[1].Priority); + + Assert.Equal("Plugin ", releaseNote.Sections[1].Name); + Assert.Equal("This description is specific to plugin section.", releaseNote.Sections[1].Summary); + Assert.Equal("http://getglimpse.com/release/icon/mvc.png", releaseNote.Sections[1].Icon); + Assert.Equal(2, releaseNote.Sections[1].Items.Count); + + Assert.Equal("*Timeline*: Comes with an additional grid view to show the same data.", releaseNote.Sections[1].Items[0].Summary); + Assert.Equal("Changed", releaseNote.Sections[1].Items[0].Category); + Assert.Equal(1, releaseNote.Sections[1].Items[1].Priority); + + Assert.Equal("*Ajax*: Fix that crashed poll in Chrome and IE due to log/trace statement.", releaseNote.Sections[1].Items[1].Summary); + Assert.Equal("Fix", releaseNote.Sections[1].Items[1].Category); + Assert.Equal(1, releaseNote.Sections[1].Items[1].Priority); + Assert.Equal("i1234", releaseNote.Sections[1].Items[1].TaskId); + Assert.Equal("http://getglimpse.com", releaseNote.Sections[1].Items[1].TaskLink); + } + + [Fact] + public void Parse_Real_NVika() + { + // act + var releaseNote = SemanticReleaseNotesParser.Parse(NVikaReleaseNotes); + + // assert + Assert.Equal(@" + +Commits: [19556f025b...0203ea9a43](https://github.com/laedit/vika/compare/19556f025b...0203ea9a43)", releaseNote.Summary); + Assert.Equal(3, releaseNote.Items.Count); + + Assert.Equal("enhancement", releaseNote.Items[0].Category); + Assert.Equal("[#9](https://github.com/laedit/vika/issues/9) - Handle multiple report files", releaseNote.Items[0].Summary); + + Assert.Equal("enhancement", releaseNote.Items[1].Category); + Assert.Equal("[#2](https://github.com/laedit/vika/issues/2) - Support AppVeyor", releaseNote.Items[1].Summary); + + Assert.Equal("enhancement", releaseNote.Items[2].Category); + Assert.Equal("[#1](https://github.com/laedit/vika/issues/1) - Support InspectCode", releaseNote.Items[2].Summary); + } + + private TextReader GetTextReader(string input) + { + return new StringReader(input); + } + + private const string Syntax_Summaries = @"This is a _project_ summary with two paragraphs. +Lorem ipsum dolor sit amet consectetuer **adipiscing** elit. +Aliquam hendreritmi posuere lectus. + +Vestibulum `enim wisi` viverra nec fringilla in laoreet +vitae risus. Donec sit amet nisl. Aliquam [semper](?) ipsum +sit amet velit."; + + private const string Syntax_Items = @" - This is the _first_ *list* item. + - This is the **second** __list__ item. + - This is the `third` list item. + - This is the [forth](?) list item."; + + private const string Syntax_Sections = @"# Section +This is the summary for Section. + - This is a Section scoped first list item. + - This is a Section scoped second list item. + +# Other Section +This is the summary for Other Section. + - This is a Other Section scoped first list item. + - This is a Other Section scoped second list item. + "; + + private const string Syntax_Priority = @" 1. This is a High priority list item. + 1. This is a High priority list item. + 2. This is a Normal priority list item. + 1. This is a High priority list item. + 2. This is a Normal priority list item. + 3. This is a Minor priority list item. + 3. This is a Minor priority list item. + "; + + private const string Syntax_Category = @" - This is a +New list item. + - This is a +Fix list item. + - This is a +Change list item. + - +New features are everyone's favorites. + - This is a list item for a +Developer. + - This is a +Super-Special custom list item. + - This is a +o ne letter category. + "; + + private const string ExampleA = @"Incremental release designed to provide an update to some of the core plugins. + + - Release Checker: Now gives you a breakdown of exactly what you are missing. +New + - Structured Layout: An alternative layout engine that allows developers to control layout. +New + - Timeline: Comes with an additional grid view to show the same data. +Changed + - Ajax: Fix that crashed poll in Chrome and IE due to log/trace statement. +Fix"; + + private const string ExampleB = @"Incremental release designed to provide an update to some of the core plugins. + +# System + - *Release Checker*: Now gives you a breakdown of exactly what you are missing. +New + - *Structured Layout*: An alternative layout engine that allows developers to control layout. +New + +# Plugin + - *Timeline*: Comes with an additional grid view to show the same data. +Changed + - *Ajax*: Fix that crashed poll in Chrome and IE due to log/trace statement. +Fix"; + + private const string ExampleC = @"Incremental release designed to provide an update to some of the core plugins. + - *Example*: You can have global issues that aren't grouped to a section + +# System +This description is specific to system section. + - *Release Checker*: Now gives you a +new breakdown of exactly what you are missing. + - *Structured Layout*: A +new alternative layout engine that allows developers to control layout. + +# Plugin +This description is specific to plugin section. + - *Timeline*: Comes with an additional grid view to show the same data. +Changed + - *Ajax*: +Fix that crashed poll in Chrome and IE due to log/trace statement. [[i1234][http://getglimpse.com]]"; + + private const string ExampleD = @"Incremental release designed to provide an update to some of the core plugins. + 1. *Example*: You can have global issues that aren't grouped to a section + +# System [[icon][http://getglimpse.com/release/icon/core.png]] +This description is specific to system section. + 3. *Release Checker*: Now gives you a breakdown of exactly what you are missing. +New + 2. *Structured Layout*: An alternative layout engine that allows developers to control layout. +New + +# Plugin [[icon][http://getglimpse.com/release/icon/mvc.png]] +This description is specific to plugin section. + 1. *Timeline*: Comes with an additional grid view to show the same data. +Changed + 1. *Ajax*: Fix that crashed poll in Chrome and IE due to log/trace statement. +Fix [[i1234][http://getglimpse.com]]"; + + private const string NVikaReleaseNotes = @" - [#9](https://github.com/laedit/vika/issues/9) - Handle multiple report files +enhancement + - [#2](https://github.com/laedit/vika/issues/2) - Support AppVeyor +enhancement + - [#1](https://github.com/laedit/vika/issues/1) - Support InspectCode +enhancement + +Commits: [19556f025b...0203ea9a43](https://github.com/laedit/vika/compare/19556f025b...0203ea9a43) +"; + } +} diff --git a/SemanticReleaseNotesParser.Core.Tests/packages.config b/SemanticReleaseNotesParser.Core.Tests/packages.config new file mode 100644 index 0000000..bd2ba2a --- /dev/null +++ b/SemanticReleaseNotesParser.Core.Tests/packages.config @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/SemanticReleaseNotesParser.Core/Category.cs b/SemanticReleaseNotesParser.Core/Category.cs new file mode 100644 index 0000000..9948df4 --- /dev/null +++ b/SemanticReleaseNotesParser.Core/Category.cs @@ -0,0 +1,21 @@ +using DotLiquid; +using System.Collections.Generic; + +namespace SemanticReleaseNotesParser.Core +{ + /// + /// Category of a release notes + /// + public sealed class Category : Drop + { + /// + /// Name of the category + /// + public string Name { get; set; } + + /// + /// Items composing the category + /// + public List Items { get; set; } + } +} diff --git a/SemanticReleaseNotesParser.Core/Formatter/GroupBy.cs b/SemanticReleaseNotesParser.Core/Formatter/GroupBy.cs new file mode 100644 index 0000000..888347f --- /dev/null +++ b/SemanticReleaseNotesParser.Core/Formatter/GroupBy.cs @@ -0,0 +1,18 @@ +namespace SemanticReleaseNotesParser.Core +{ + /// + /// Defines how items will be grouped + /// + public enum GroupBy + { + /// + /// Group by sections + /// + Sections, + + /// + /// Group by categories + /// + Categories + } +} diff --git a/SemanticReleaseNotesParser.Core/Formatter/OutputFormat.cs b/SemanticReleaseNotesParser.Core/Formatter/OutputFormat.cs new file mode 100644 index 0000000..9e1547e --- /dev/null +++ b/SemanticReleaseNotesParser.Core/Formatter/OutputFormat.cs @@ -0,0 +1,18 @@ +namespace SemanticReleaseNotesParser.Core +{ + /// + /// Defines the output format + /// + public enum OutputFormat + { + /// + /// Html + /// + Html, + + /// + /// Markdown + /// + Markdown + } +} diff --git a/SemanticReleaseNotesParser.Core/Formatter/SemanticReleaseNotesFormatter.cs b/SemanticReleaseNotesParser.Core/Formatter/SemanticReleaseNotesFormatter.cs new file mode 100644 index 0000000..1fe4275 --- /dev/null +++ b/SemanticReleaseNotesParser.Core/Formatter/SemanticReleaseNotesFormatter.cs @@ -0,0 +1,126 @@ +using CommonMark; +using DotLiquid; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Reflection; + +namespace SemanticReleaseNotesParser.Core +{ + /// + /// Semantic release notes formatter + /// + public sealed class SemanticReleaseNotesFormatter + { + private static CommonMarkSettings DefaultCommonMarkSettings = new CommonMarkSettings + { + AdditionalFeatures = CommonMarkAdditionalFeatures.StrikethroughTilde, + OutputFormat = CommonMark.OutputFormat.Html + }; + + /// + /// Format a release notes + /// + /// TextWriter which will be used to writes the formatted release notes + /// Release notes to format + /// Settings used for formatting + public static void Format(TextWriter writer, ReleaseNotes releaseNotes, SemanticReleaseNotesFormatterSettings settings = null) + { + if (writer == null) + { + throw new ArgumentNullException("writer"); + } + + writer.Write(Format(releaseNotes)); + } + + /// + ///Format a release notes + /// + /// Release notes to format + /// Settings used for formatting + /// Formatted release notes + public static string Format(ReleaseNotes releaseNotes, SemanticReleaseNotesFormatterSettings settings = null) + { + if (releaseNotes == null) + { + throw new ArgumentNullException("releaseNotes"); + } + + if (settings == null) + { + settings = SemanticReleaseNotesFormatterSettings.Default; + } + + // template selection + string template = settings.LiquidTemplate ?? GetLiquidTemplate(settings); + + // liquid rendering + var liquidTemplate = Template.Parse(template); + + // process categories + var categories = new Dictionary>(); + ProcessCategories(categories, releaseNotes.Items); + + foreach (var section in releaseNotes.Sections) + { + ProcessCategories(categories, section.Items); + } + + var processedCategories = categories.Select(x => new Category { Name = x.Key, Items = x.Value }).ToList(); + + var itemsWithoutCategory = new List(releaseNotes.Items.Where(i => string.IsNullOrEmpty(i.Category))); + releaseNotes.Sections.ForEach(s => itemsWithoutCategory.AddRange(s.Items.Where(i => string.IsNullOrEmpty(i.Category)))); + + string result = liquidTemplate.Render(Hash.FromAnonymousObject(new { release_notes = releaseNotes, lcb = "{", rcb = "}", categories = processedCategories, items_without_categories = itemsWithoutCategory })); + + if (settings.OutputFormat == OutputFormat.Markdown) + { + return result; + } + + // convert to HTML + if (releaseNotes.Items.Any(i => i.Priority > 0) || releaseNotes.Sections.Any(s => s.Items.Any(i => i.Priority > 0))) + { + throw new InvalidOperationException("The priorities for items are not supported currently for Html output."); + } + return CommonMarkConverter.Convert(result, DefaultCommonMarkSettings); + } + + private static void ProcessCategories(Dictionary> categories, List items) + { + foreach (var item in items) + { + if (!string.IsNullOrEmpty(item.Category)) + { + var categoryName = FirstLetterToUpper(item.Category); + if (!categories.ContainsKey(categoryName)) + { + categories.Add(categoryName, new List()); + } + categories[categoryName].Add(item); + } + } + } + + private static string FirstLetterToUpper(string str) + { + return char.ToUpper(str[0]) + str.Substring(1); + } + + private static string GetLiquidTemplate(SemanticReleaseNotesFormatterSettings settings) + { + string templateName = "SemanticReleaseNotesParser.Core.Resources.GroupBySections.liquid"; + if (settings.GroupBy == GroupBy.Categories) + { + templateName = "SemanticReleaseNotesParser.Core.Resources.GroupByCategories.liquid"; + } + + using (var reader = new StreamReader(Assembly.GetExecutingAssembly().GetManifestResourceStream(templateName))) + { + return reader.ReadToEnd(); + } + } + } +} diff --git a/SemanticReleaseNotesParser.Core/Formatter/SemanticReleaseNotesFormatterSettings.cs b/SemanticReleaseNotesParser.Core/Formatter/SemanticReleaseNotesFormatterSettings.cs new file mode 100644 index 0000000..78d146e --- /dev/null +++ b/SemanticReleaseNotesParser.Core/Formatter/SemanticReleaseNotesFormatterSettings.cs @@ -0,0 +1,30 @@ +namespace SemanticReleaseNotesParser.Core +{ + /// + /// Settings for semantic release notes formatter + /// + public sealed class SemanticReleaseNotesFormatterSettings + { + private static SemanticReleaseNotesFormatterSettings _default = new SemanticReleaseNotesFormatterSettings(); + + /// + /// Defines the output format + /// + public OutputFormat OutputFormat { get; set; } + + /// + /// Defines how items will be grouped + /// + public GroupBy GroupBy { get; set; } + + /// + /// Path to a liquid template to use for the conversion. Overrides OutputFormat and GroupBy. + /// + public string LiquidTemplate { get; set; } + + internal static SemanticReleaseNotesFormatterSettings Default + { + get { return _default; } + } + } +} diff --git a/SemanticReleaseNotesParser.Core/Item.cs b/SemanticReleaseNotesParser.Core/Item.cs new file mode 100644 index 0000000..db05b09 --- /dev/null +++ b/SemanticReleaseNotesParser.Core/Item.cs @@ -0,0 +1,35 @@ +using DotLiquid; + +namespace SemanticReleaseNotesParser.Core +{ + /// + /// Item of a release notes + /// + public sealed class Item : Drop + { + /// + /// Task id + /// + public string TaskId { get; set; } + + /// + /// Task link + /// + public string TaskLink { get; set; } + + /// + /// Category + /// + public string Category { get; set; } + + /// + /// Priority + /// + public int Priority { get; set; } + + /// + /// Summary + /// + public string Summary { get; set; } + } +} diff --git a/SemanticReleaseNotesParser.Core/Properties/AssemblyInfo.cs b/SemanticReleaseNotesParser.Core/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..dfaa104 --- /dev/null +++ b/SemanticReleaseNotesParser.Core/Properties/AssemblyInfo.cs @@ -0,0 +1,36 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("SemanticReleaseNotesParser.Core")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("SemanticReleaseNotesParser.Core")] +[assembly: AssemblyCopyright("Copyright © 2015")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("589794ec-4420-4b76-9b88-924d15148d51")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/SemanticReleaseNotesParser.Core/ReleaseNotes.cs b/SemanticReleaseNotesParser.Core/ReleaseNotes.cs new file mode 100644 index 0000000..0acf181 --- /dev/null +++ b/SemanticReleaseNotesParser.Core/ReleaseNotes.cs @@ -0,0 +1,35 @@ +using DotLiquid; +using System.Collections.Generic; + +namespace SemanticReleaseNotesParser.Core +{ + /// + /// Release notes + /// + public sealed class ReleaseNotes : Drop + { + /// + /// Summary of the release notes + /// + public string Summary { get; set; } + + /// + /// Sections of the release notes + /// + public List
Sections { get; set; } + + /// + /// Items of the release notes + /// + public List Items { get; set; } + + /// + /// Instantiates a new ReleaseNotes + /// + public ReleaseNotes() + { + Sections = new List
(); + Items = new List(); + } + } +} diff --git a/SemanticReleaseNotesParser.Core/Resources/GroupByCategories.liquid b/SemanticReleaseNotesParser.Core/Resources/GroupByCategories.liquid new file mode 100644 index 0000000..f9bdc9a --- /dev/null +++ b/SemanticReleaseNotesParser.Core/Resources/GroupByCategories.liquid @@ -0,0 +1,12 @@ +{{ release_notes.summary }} +{%- for item in items_without_categories -%} + {% if item.priority > 0 -%} {{ item.priority }}. {%- else -%} - {%- endif %} {{ item.summary }} {%- if item.task_id %} [{{ item.task_id }}]({{ item.task_link }}) {%- endif -%} + +{%- endfor -%} +{%- for category in categories -%} +# {{ category.name }} +{%- for item in category.items -%} + {% if item.priority > 0 -%} {{ item.priority }}. {%- else -%} - {%- endif %} {{ item.summary }} {%- if item.task_id %} [{{ item.task_id }}]({{ item.task_link }}) {%- endif -%} + +{%- endfor -%} +{%- endfor -%} \ No newline at end of file diff --git a/SemanticReleaseNotesParser.Core/Resources/GroupBySections.liquid b/SemanticReleaseNotesParser.Core/Resources/GroupBySections.liquid new file mode 100644 index 0000000..0a33aa2 --- /dev/null +++ b/SemanticReleaseNotesParser.Core/Resources/GroupBySections.liquid @@ -0,0 +1,12 @@ +{{ release_notes.summary }} +{%- for item in release_notes.items -%} + {% if item.priority > 0 -%} {{ item.priority }}. {%- else -%} - {%- endif %} {% if item.category -%}{{ lcb }}{{ item.category }}{{ rcb }} {% endif %}{{ item.summary }} {%- if item.task_id %} [{{ item.task_id }}]({{ item.task_link }}) {%- endif -%} + +{%- endfor -%} +{%- for section in release_notes.sections -%} +# {{ section.name }} +{%- for item in section.items -%} + {% if item.priority > 0 -%} {{ item.priority }}. {%- else -%} - {%- endif %} {% if item.category -%}{{ lcb }}{{ item.category }}{{ rcb }} {% endif %}{{ item.summary }} {%- if item.task_id %} [{{ item.task_id }}]({{ item.task_link }}) {%- endif -%} + +{%- endfor -%} +{%- endfor -%} \ No newline at end of file diff --git a/SemanticReleaseNotesParser.Core/Section.cs b/SemanticReleaseNotesParser.Core/Section.cs new file mode 100644 index 0000000..0bf51fb --- /dev/null +++ b/SemanticReleaseNotesParser.Core/Section.cs @@ -0,0 +1,39 @@ +using DotLiquid; +using System.Collections.Generic; + +namespace SemanticReleaseNotesParser.Core +{ + /// + /// Sections of a release notes + /// + public sealed class Section : Drop + { + /// + /// Name of the section + /// + public string Name { get; set; } + + /// + /// Summary of the section + /// + public string Summary { get; set; } + + /// + /// Items composing the section + /// + public List Items { get; set; } + + /// + /// Icon of the section + /// + public string Icon { get; set; } + + /// + /// Instantiates a new Section + /// + public Section() + { + Items = new List(); + } + } +} diff --git a/SemanticReleaseNotesParser.Core/SemanticReleaseNotesParser.Core.csproj b/SemanticReleaseNotesParser.Core/SemanticReleaseNotesParser.Core.csproj new file mode 100644 index 0000000..86723f6 --- /dev/null +++ b/SemanticReleaseNotesParser.Core/SemanticReleaseNotesParser.Core.csproj @@ -0,0 +1,76 @@ + + + + + Debug + AnyCPU + {982453C5-147D-435E-B70A-E25E6C2EE748} + Library + Properties + SemanticReleaseNotesParser.Core + SemanticReleaseNotesParser.Core + v4.0 + 512 + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + bin\Debug\SemanticReleaseNotesParser.Core.XML + true + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + true + bin\Release\SemanticReleaseNotesParser.Core.XML + + + + ..\packages\CommonMark.NET.0.8.3\lib\net40-client\CommonMark.dll + + + ..\packages\DotLiquid.1.8.0\lib\NET40\DotLiquid.dll + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/SemanticReleaseNotesParser.Core/SemanticReleaseNotesParser.cs b/SemanticReleaseNotesParser.Core/SemanticReleaseNotesParser.cs new file mode 100644 index 0000000..9852ee8 --- /dev/null +++ b/SemanticReleaseNotesParser.Core/SemanticReleaseNotesParser.cs @@ -0,0 +1,175 @@ +using System; +using System.IO; +using System.Linq; +using System.Text.RegularExpressions; + +namespace SemanticReleaseNotesParser.Core +{ + /// + /// Semantic Release Notes Parser + /// + public sealed class SemanticReleaseNotesParser + { + private static readonly Regex LinkRegex = new Regex(@"\[\[(\S+)\]\[(\S+)\]\]", RegexOptions.Compiled | RegexOptions.IgnoreCase); + private static readonly Regex SectionRegex = new Regex(@"^# ([\w\s*]*)", RegexOptions.Compiled | RegexOptions.IgnoreCase); + private static readonly Regex PriorityRegex = new Regex(@"^ [\-\+\*]|([123])\. ", RegexOptions.Compiled | RegexOptions.IgnoreCase); + private static readonly Regex CategoryRegex = new Regex(@"\+([^\s]+)", RegexOptions.Compiled | RegexOptions.IgnoreCase); + private static readonly Regex SummaryRegex = new Regex(@"^[a-zA-Z0-9]", RegexOptions.Compiled | RegexOptions.IgnoreCase); + + /// + /// Parse a release notes from a stream + /// + /// Reader of release notes + /// A parsed release notes + public static ReleaseNotes Parse(TextReader reader) + { + if (reader == null) + { + throw new ArgumentNullException("reader"); + } + + return Parse(reader.ReadToEnd()); + } + + /// + /// Parse a release notes + /// + /// Raw release notes + /// A parsed release notes + public static ReleaseNotes Parse(string rawReleaseNotes) + { + if (string.IsNullOrEmpty(rawReleaseNotes)) + { + throw new ArgumentNullException("rawReleaseNotes"); + } + + var releaseNotes = new ReleaseNotes(); + + var rawLines = rawReleaseNotes.Split(new[] { Environment.NewLine }, StringSplitOptions.None); + + for (int i = 0; i < rawLines.Length; i++) + { + var rawLine = rawLines[i]; + var matched = false; + + // Process the line + matched = ProcessSection(rawLine, releaseNotes); + + if (!matched) + { + matched = ProcessItem(rawLine, releaseNotes); + } + + if (!matched) + { + string nextInput = string.Empty; + if (i + 1 < rawLines.Length) + { + nextInput = rawLines[i + 1]; + } + ProcessPrimary(rawLine, releaseNotes, nextInput); + } + } + + return releaseNotes; + } + + private static void ProcessPrimary(string input, ReleaseNotes releaseNotes, string nextInput) + { + input = input.Trim(); + if (string.IsNullOrEmpty(input) && !string.IsNullOrEmpty(nextInput) && SummaryRegex.IsMatch(nextInput)) + { + input = Environment.NewLine + Environment.NewLine; + } + + if (releaseNotes.Sections.Count > 0) + { + var lastSection = releaseNotes.Sections.Last(); + lastSection.Summary = (lastSection.Summary ?? string.Empty) + input; + } + else + { + releaseNotes.Summary = (releaseNotes.Summary ?? string.Empty) + input; + } + } + + private static bool ProcessItem(string input, ReleaseNotes releaseNotes) + { + var match = PriorityRegex.Match(input); + if (match.Success) + { + var item = new Item(); + + // Priority + int priority; + if (!string.IsNullOrEmpty(match.Groups[1].Value) && Int32.TryParse(match.Groups[1].Value, out priority)) + { + item.Priority = priority; + } + input = PriorityRegex.Replace(input, string.Empty); + + // category + var category = CategoryRegex.Match(input); + if (category.Success && !string.IsNullOrEmpty(category.Groups[1].Value)) + { + item.Category = category.Groups[1].Value; + input = CategoryRegex.Replace(input, string.Empty); + } + + // link + var link = GetLink(input); + if (!string.IsNullOrEmpty(link.Item1)) + { + item.TaskId = link.Item1; + item.TaskLink = link.Item2; + input = LinkRegex.Replace(input, string.Empty); + } + + // summary + item.Summary = input.Trim(); + + if (releaseNotes.Sections.Count == 0) + { + releaseNotes.Items.Add(item); + } + else + { + releaseNotes.Sections.Last().Items.Add(item); + } + + return true; + } + return false; + } + + private static bool ProcessSection(string input, ReleaseNotes releaseNotes) + { + var match = SectionRegex.Match(input); + if (match.Success) + { + var section = new Section(); + section.Name = match.Groups[1].Value; + + var link = GetLink(input); + if (!string.IsNullOrEmpty(link.Item1) && link.Item1.Equals("icon", StringComparison.OrdinalIgnoreCase)) + { + section.Icon = link.Item2; + } + + releaseNotes.Sections.Add(section); + return true; + } + return false; + } + + private static Tuple GetLink(string input) + { + var match = LinkRegex.Match(input); + if (match.Success) + { + return Tuple.Create(match.Groups[1].Value, match.Groups[2].Value); + } + return Tuple.Create(string.Empty, string.Empty); + } + } +} diff --git a/SemanticReleaseNotesParser.Core/packages.config b/SemanticReleaseNotesParser.Core/packages.config new file mode 100644 index 0000000..956c5bc --- /dev/null +++ b/SemanticReleaseNotesParser.Core/packages.config @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/SemanticReleaseNotesParser.Tests/ArgumentsTest.cs b/SemanticReleaseNotesParser.Tests/ArgumentsTest.cs new file mode 100644 index 0000000..27f30ac --- /dev/null +++ b/SemanticReleaseNotesParser.Tests/ArgumentsTest.cs @@ -0,0 +1,187 @@ +using SemanticReleaseNotesParser.Core; +using System.IO; +using System.Text; +using Xunit; + +namespace SemanticReleaseNotesParser.Tests +{ + public class ArgumentsTest + { + [Fact] + public void WriteHelp_All() + { + // arrange + var arguments = new Arguments(); + var output = new StringBuilder(); + var writer = new StringWriter(output); + + // act + arguments.WriteOptionDescriptions(writer); + + // assert + Assert.Contains("-r, --releasenotes=VALUE", output.ToString()); + Assert.Contains("-o, --outputfile=VALUE", output.ToString()); + Assert.Contains("-t, --outputtype=VALUE", output.ToString()); + Assert.Contains("--template=VALUE", output.ToString()); + Assert.Contains("--debug", output.ToString()); + Assert.Contains("-h, -?, --help", output.ToString()); + Assert.Contains("-f, --outputformat=VALUE", output.ToString()); + } + + [Fact] + public void ParseArguments_NoArgument() + { + // act + var arguments = Arguments.ParseArguments(new string[0]); + + // assert + Assert.False(arguments.Debug); + Assert.False(arguments.Help); + Assert.Equal(OutputFormat.Html, arguments.OutputFormat); + Assert.Equal(OutputType.File, arguments.OutputType); + Assert.Equal("ReleaseNotes.md", arguments.ReleaseNotesPath); + Assert.Equal("ReleaseNotes.html", arguments.ResultFilePath); + Assert.Null(arguments.TemplatePath); + } + + [Fact] + public void ParseArguments_Debug() + { + // act + var arguments = Arguments.ParseArguments(new[] { "--debug" }); + + // assert + Assert.True(arguments.Debug); + } + + [Fact] + public void ParseArguments_Help_h() + { + // act + var arguments = Arguments.ParseArguments(new[] { "-h" }); + + // assert + Assert.True(arguments.Help); + } + + [Fact] + public void ParseArguments_Help_QuestionMark() + { + // act + var arguments = Arguments.ParseArguments(new[] { "-?" }); + + // assert + Assert.True(arguments.Help); + } + + [Fact] + public void ParseArguments_Help_Help() + { + // act + var arguments = Arguments.ParseArguments(new[] { "--help" }); + + // assert + Assert.True(arguments.Help); + } + + [Fact] + public void ParseArguments_ReleaseNotesPath_r() + { + // act + var arguments = Arguments.ParseArguments(new[] { "-r=myReleases.md" }); + + // assert + Assert.Equal("myReleases.md", arguments.ReleaseNotesPath); + } + + [Fact] + public void ParseArguments_ReleaseNotesPath_releasenotes() + { + // act + var arguments = Arguments.ParseArguments(new[] { "--releasenotes=myReleases.md" }); + + // assert + Assert.Equal("myReleases.md", arguments.ReleaseNotesPath); + } + + [Fact] + public void ParseArguments_ResultFilePath_o() + { + // act + var arguments = Arguments.ParseArguments(new[] { "-o=myReleases.html" }); + + // assert + Assert.Equal("myReleases.html", arguments.ResultFilePath); + } + + [Fact] + public void ParseArguments_ResultFilePath_outputfile() + { + // act + var arguments = Arguments.ParseArguments(new[] { "--outputfile=myReleases.html" }); + + // assert + Assert.Equal("myReleases.html", arguments.ResultFilePath); + } + + [Fact] + public void ParseArguments_OutputTypeh_t() + { + // act + var arguments = Arguments.ParseArguments(new[] { "-t=environment" }); + + // assert + Assert.Equal(OutputType.Environment, arguments.OutputType); + } + + [Fact] + public void ParseArguments_OutputType_outputfile() + { + // act + var arguments = Arguments.ParseArguments(new[] { "--outputtype=environment" }); + + // assert + Assert.Equal(OutputType.Environment, arguments.OutputType); + } + + [Fact] + public void ParseArguments_OutputFormat_f() + { + // act + var arguments = Arguments.ParseArguments(new[] { "-f=markdown" }); + + // assert + Assert.Equal(OutputFormat.Markdown, arguments.OutputFormat); + } + + [Fact] + public void ParseArguments_OutputFormat_outputformat() + { + // act + var arguments = Arguments.ParseArguments(new[] { "--outputformat=markdown" }); + + // assert + Assert.Equal(OutputFormat.Markdown, arguments.OutputFormat); + } + + [Fact] + public void ParseArguments_Template() + { + // act + var arguments = Arguments.ParseArguments(new[] { "--template=template.liquid" }); + + // assert + Assert.Equal("template.liquid", arguments.TemplatePath); + } + + [Fact] + public void ParseArguments_FirstAdditionalArgument_IsReleaseNotesPath() + { + // act + var arguments = Arguments.ParseArguments(new[] { "myReleases.md" }); + + // assert + Assert.Equal("myReleases.md", arguments.ReleaseNotesPath); + } + } +} diff --git a/SemanticReleaseNotesParser.Tests/BuildServers/AppVeyorTest.cs b/SemanticReleaseNotesParser.Tests/BuildServers/AppVeyorTest.cs new file mode 100644 index 0000000..b9217ee --- /dev/null +++ b/SemanticReleaseNotesParser.Tests/BuildServers/AppVeyorTest.cs @@ -0,0 +1,104 @@ +using NSubstitute; +using SemanticReleaseNotesParser.Abstractions; +using SemanticReleaseNotesParser.BuildServers; +using System.Collections.Generic; +using System.IO; +using System.Text; +using Xunit; + +namespace SemanticReleaseNotesParser.Tests.BuildServers +{ + public class AppVeyorTest + { + [Fact] + public void CanApplyToCurrentContext_True() + { + // arrange + var buildServer = new AppVeyor(GetEnvironment(), GetWebClientFactory()); + + // act + var canApply = buildServer.CanApplyToCurrentContext(); + + // assert + Assert.True(canApply); + } + + [Fact] + public void CanApplyToCurrentContext_False() + { + // arrange + var buildServer = new AppVeyor(GetEnvironment(false), GetWebClientFactory()); + + // act + var canApply = buildServer.CanApplyToCurrentContext(); + + // assert + Assert.False(canApply); + } + + [Fact] + public void SetEnvironmentVariable() + { + // arrange + var buildServer = new AppVeyor(GetEnvironment(), GetWebClientFactory()); + + // act + buildServer.SetEnvironmentVariable("name", "value"); + + // assert + Assert.False(_environmentVariables.ContainsKey("name")); + Assert.Equal("api/build/variables", _address); + Assert.Equal("POST", _method); + Assert.Equal("{ \"name\": \"name\", \"value\": \"value\" }", _uploadedData); + Assert.Equal("Adding AppVeyor environment variable: name.", _logs.ToString().Trim()); + } + + private StringBuilder _logs; + + public AppVeyorTest() + { + _logs = new StringBuilder(); + Logger.SetWriter(new StringWriter(_logs)); + } + + private Dictionary _environmentVariables; + + private IEnvironment GetEnvironment(bool isOnAppVeyor = true) + { + _environmentVariables = new Dictionary(); + + var environment = Substitute.For(); + environment.When(e => e.SetEnvironmentVariable(Arg.Any(), Arg.Any())).Do(ci => _environmentVariables.Add((string)ci.Args()[0], (string)ci.Args()[1])); + + environment.GetEnvironmentVariable("APPVEYOR_API_URL").Returns("http://localhost:8080"); + + if (isOnAppVeyor) + { + environment.GetEnvironmentVariable("APPVEYOR").Returns("TRUE"); + } + + return environment; + } + + private string _uploadedData; + private string _address; + private string _method; + + private IWebClientFactory GetWebClientFactory() + { + var webClient = Substitute.For(); + webClient.When(wc => wc.UploadData(Arg.Any(), Arg.Any(), Arg.Any())) + .Do(ci => + { + _address = (string)ci.Args()[0]; + _method = (string)ci.Args()[1]; + _uploadedData = Encoding.UTF8.GetString((byte[])ci.Args()[2]); + }); + + var factory = Substitute.For(); + factory.Create(Arg.Any()).Returns(webClient); + + return factory; + } + } +} diff --git a/SemanticReleaseNotesParser.Tests/BuildServers/LocalBuildServerTest.cs b/SemanticReleaseNotesParser.Tests/BuildServers/LocalBuildServerTest.cs new file mode 100644 index 0000000..09df1f1 --- /dev/null +++ b/SemanticReleaseNotesParser.Tests/BuildServers/LocalBuildServerTest.cs @@ -0,0 +1,60 @@ +using NSubstitute; +using SemanticReleaseNotesParser.Abstractions; +using SemanticReleaseNotesParser.BuildServers; +using System.Collections.Generic; +using System.IO; +using System.Text; +using Xunit; + +namespace SemanticReleaseNotesParser.Tests.BuildServers +{ + public class LocalBuildServerTest + { + [Fact] + public void CanApplyToCurrentContext() + { + // arrange + var buildServer = new LocalBuildServer(GetEnvironment()); + + // act + var canApply = buildServer.CanApplyToCurrentContext(); + + // assert + Assert.True(canApply); + } + + [Fact] + public void SetEnvironmentVariable() + { + // arrange + var buildServer = new LocalBuildServer(GetEnvironment()); + + // act + buildServer.SetEnvironmentVariable("name", "value"); + + // assert + Assert.Equal("value", _environmentVariables["name"]); + Assert.Equal("Adding local environment variable: name.", _logs.ToString().Trim()); + } + + private StringBuilder _logs; + + public LocalBuildServerTest() + { + _logs = new StringBuilder(); + Logger.SetWriter(new StringWriter(_logs)); + } + + private Dictionary _environmentVariables; + + private IEnvironment GetEnvironment() + { + _environmentVariables = new Dictionary(); + + var environment = Substitute.For(); + environment.When(e => e.SetEnvironmentVariable(Arg.Any(), Arg.Any())).Do(ci => _environmentVariables.Add((string)ci.Args()[0], (string)ci.Args()[1])); + + return environment; + } + } +} diff --git a/SemanticReleaseNotesParser.Tests/LoggerTest.cs b/SemanticReleaseNotesParser.Tests/LoggerTest.cs new file mode 100644 index 0000000..19e664b --- /dev/null +++ b/SemanticReleaseNotesParser.Tests/LoggerTest.cs @@ -0,0 +1,86 @@ +using System; +using System.IO; +using System.Text; +using Xunit; + +namespace SemanticReleaseNotesParser.Tests +{ + public class LoggerTest : IDisposable + { + private StringBuilder _logs; + + [Fact] + public void Info_TextWriter_Null_ThrowException() + { + // act + var exception = Assert.Throws(() => Logger.Info("test info")); + + // assert + Assert.Equal("The writer must be set with the 'SetWriter' method first.", exception.Message); + } + + [Fact] + public void Info() + { + // arrange + Logger.SetWriter(GetLogOutput()); + + // act + Logger.Info("test '{0}'", "info"); + + // assert + Assert.Equal("test 'info'", _logs.ToString().Trim()); + } + + [Fact] + public void Error() + { + // arrange + Logger.SetWriter(GetLogOutput()); + + // act + Logger.Error("test '{0}'", "error"); + + // assert + Assert.Equal("test 'error'", _logs.ToString().Trim()); + } + + [Fact] + public void Debug_WithoutAddingProperCategory_ShouldNotLog() + { + // arrange + Logger.SetWriter(GetLogOutput()); + + // act + Logger.Debug("test '{0}'", "debug"); + + // assert + Assert.Equal(string.Empty, _logs.ToString()); + } + + [Fact] + public void Debug() + { + // arrange + Logger.SetWriter(GetLogOutput()); + Logger.AddCategory("debug"); + + // act + Logger.Debug("test '{0}'", "debug"); + + // assert + Assert.Equal("test 'debug'", _logs.ToString().Trim()); + } + + private TextWriter GetLogOutput() + { + _logs = new StringBuilder(); + return new StringWriter(_logs); + } + + public void Dispose() + { + Logger.Reset(); + } + } +} diff --git a/SemanticReleaseNotesParser.Tests/ProgramTest.cs b/SemanticReleaseNotesParser.Tests/ProgramTest.cs new file mode 100644 index 0000000..485030d --- /dev/null +++ b/SemanticReleaseNotesParser.Tests/ProgramTest.cs @@ -0,0 +1,317 @@ +using NSubstitute; +using SemanticReleaseNotesParser.Abstractions; +using System; +using System.Collections.Generic; +using System.IO; +using System.IO.Abstractions; +using System.IO.Abstractions.TestingHelpers; +using System.Text; +using Xunit; + +namespace SemanticReleaseNotesParser.Tests +{ + public class ProgramTest : IDisposable + { + [Fact] + public void Run_Help() + { + // arrange + Program.FileSystem = GetFileSystem(); + Program.Environment = GetEnvironment(); + Program.WebClientFactory = GetWebClientFactory(); + + // act + Program.Main(new[] { "-?" }); + + // assert + Assert.Equal(1, _exitCode); + Assert.Contains("-r", _output.ToString()); + Assert.Contains("--releasenotes", _output.ToString()); + } + + [Fact] + public void Run_Default() + { + // arrange + Program.FileSystem = GetFileSystem(); + Program.Environment = GetEnvironment(); + Program.WebClientFactory = GetWebClientFactory(); + + // act + Program.Main(new string[0]); + + // assert + Assert.Equal(0, _exitCode); + Assert.Equal(ExpectedHtml, Program.FileSystem.File.ReadAllText("ReleaseNotes.html").Trim()); + Assert.DoesNotContain("File output", _output.ToString()); + } + + [Fact] + public void Run_Default_Debug() + { + // arrange + Program.FileSystem = GetFileSystem(); + Program.Environment = GetEnvironment(); + Program.WebClientFactory = GetWebClientFactory(); + + // act + Program.Main(new[] { "--debug" }); + + // assert + Assert.Equal(0, _exitCode); + Assert.Equal(ExpectedHtml, Program.FileSystem.File.ReadAllText("ReleaseNotes.html").Trim()); + Assert.Contains("File output", _output.ToString()); + } + + [Fact] + public void Run_Default_FileNotExists_Exception() + { + // arrange + Program.FileSystem = GetFileSystem(false); + Program.Environment = GetEnvironment(); + Program.WebClientFactory = GetWebClientFactory(); + + // act + Program.Main(new string[0]); + + // assert + Assert.Equal(1, _exitCode); + Assert.Contains("Release notes file 'ReleaseNotes.md' does not exists", _output.ToString()); + } + + [Fact] + public void Run_Custom_ReleaseNotes_And_OutputFile() + { + // arrange + Program.FileSystem = GetFileSystem(true, "myreleansenotes.md"); + Program.Environment = GetEnvironment(); + Program.WebClientFactory = GetWebClientFactory(); + + // act + Program.Main(new[] { "-r=myreleansenotes.md", "-o=MyReleaseNotes.html" }); + + // assert + Assert.Equal(0, _exitCode); + Assert.Equal(ExpectedHtml, Program.FileSystem.File.ReadAllText("MyReleaseNotes.html").Trim()); + } + + [Fact] + public void Run_Custom_Template() + { + // arrange + Program.FileSystem = GetFileSystem(true, "ReleaseNotes.md", "template.liquid", CustomTemplate); + Program.Environment = GetEnvironment(); + Program.WebClientFactory = GetWebClientFactory(); + + // act + Program.Main(new[] { "--template=template.liquid" }); + + // assert + Assert.Equal(0, _exitCode); + Assert.Equal(ExpectedCustomHtml, Program.FileSystem.File.ReadAllText("ReleaseNotes.html").Trim()); + } + + [Fact] + public void Run_Custom_Template_Does_Not_Exists() + { + // arrange + Program.FileSystem = GetFileSystem(); + Program.Environment = GetEnvironment(); + Program.WebClientFactory = GetWebClientFactory(); + + // act + Program.Main(new[] { "--template=template.liquid" }); + + // assert + Assert.Equal(1, _exitCode); + Assert.Contains("Template file 'template.liquid' does not exists", _output.ToString()); + } + + [Fact] + public void Run_Default_Output_Format_Markdown() + { + // arrange + Program.FileSystem = GetFileSystem(); + Program.Environment = GetEnvironment(); + Program.WebClientFactory = GetWebClientFactory(); + + // act + Program.Main(new[] { "-f=markdown" }); + + // assert + Assert.Equal(0, _exitCode); + Assert.Equal(ExpectedMarkdow, Program.FileSystem.File.ReadAllText("ReleaseNotes.html").Trim()); + } + + [Fact] + public void Run_Handle_Global_Exception() + { + // arrange + var fileSystem = Substitute.For(); + fileSystem.File.Returns(ci => { throw new InvalidOperationException("Boom when accessing File"); }); + Program.FileSystem = fileSystem; + Program.Environment = GetEnvironment(); + Program.WebClientFactory = GetWebClientFactory(); + + // act + Program.Main(new string[0]); + + // assert + Assert.Equal(1, _exitCode); + Assert.Contains("An unexpected error occurred:", _output.ToString()); + Assert.Contains("InvalidOperationException", _output.ToString()); + Assert.Contains("Boom when accessing File", _output.ToString()); + } + + [Fact] + public void Run_Environment() + { + // arrange + Program.FileSystem = GetFileSystem(); + Program.Environment = GetEnvironment(); + Program.WebClientFactory = GetWebClientFactory(); + + // act + Program.Main(new[] { "-t=environment" }); + + // assert + Assert.Equal(0, _exitCode); + Assert.False(Program.FileSystem.File.Exists("ReleaseNotes.html")); + Assert.Equal(ExpectedHtml, _environmentVariables["SemanticReleaseNotes"].Trim()); + } + + [Fact] + public void Run_Environment_AppVeyor() + { + // arrange + Program.FileSystem = GetFileSystem(); + Program.Environment = GetEnvironment(true); + Program.WebClientFactory = GetWebClientFactory(); + + // act + Program.Main(new[] { "-t=environment" }); + + // assert + Assert.Equal(0, _exitCode); + Assert.False(Program.FileSystem.File.Exists("ReleaseNotes.html")); + Assert.False(_environmentVariables.ContainsKey("SemanticReleaseNotes")); + Assert.Equal(ExpectedAppVeyorData, _uploadedData); + } + + private StringBuilder _output; + + public ProgramTest() + { + _output = new StringBuilder(); + Console.SetOut(new StringWriter(_output)); + } + + private Dictionary _environmentVariables; + private int _exitCode; + + private IEnvironment GetEnvironment(bool isOnAppVeyor = false) + { + _environmentVariables = new Dictionary(); + + var environment = Substitute.For(); + environment.When(e => e.SetEnvironmentVariable(Arg.Any(), Arg.Any())).Do(ci => _environmentVariables.Add((string)ci.Args()[0], (string)ci.Args()[1])); + + environment.GetEnvironmentVariable("APPVEYOR_API_URL").Returns("http://localhost:8080"); + + if (isOnAppVeyor) + { + environment.GetEnvironmentVariable("APPVEYOR").Returns("TRUE"); + } + + environment.When(e => e.Exit(Arg.Any())).Do(ci => _exitCode = ci.Arg()); + + return environment; + } + + private string _uploadedData; + private string _address; + private string _method; + + private IWebClientFactory GetWebClientFactory() + { + var webClient = Substitute.For(); + webClient.When(wc => wc.UploadData(Arg.Any(), Arg.Any(), Arg.Any())) + .Do(ci => + { + _address = (string)ci.Args()[0]; + _method = (string)ci.Args()[1]; + _uploadedData = Encoding.UTF8.GetString((byte[])ci.Args()[2]); + }); + + var factory = Substitute.For(); + factory.Create(Arg.Any()).Returns(webClient); + + return factory; + } + + private IFileSystem GetFileSystem(bool addFile = true, string fileName = "ReleaseNotes.md", string secondFileName = null, string secondFileContent = null) + { + var fileSystem = new MockFileSystem(); + + if (addFile) + { + fileSystem.AddFile(fileName, new MockFileData(DefaultMarkdown)); + } + + if (secondFileName != null) + { + fileSystem.AddFile(secondFileName, new MockFileData(secondFileContent)); + } + + return fileSystem; + } + + public void Dispose() + { + Logger.Reset(); + } + + private const string DefaultMarkdown = @"A little summary +# System + - This is the **second** __list__ item. +new + - This is the `third` list item. +fix"; + + private const string ExpectedHtml = @"

A little summary

+

System

+
    +
  • {new} This is the second list item.
  • +
  • {fix} This is the third list item.
  • +
"; + + private const string ExpectedMarkdow = @"A little summary +# System + - {new} This is the **second** __list__ item. + - {fix} This is the `third` list item."; + + private const string CustomTemplate = @"{%- for section in release_notes.sections -%} +# {{ section.name }} +{%- for item in section.items -%} + - {% if item.category -%}{{ lcb }}{{ item.category }}{{ rcb }} {% endif %}{{ item.summary }} {%- if item.task_id %} [{{ item.task_id }}]({{ item.task_link }}) {%- endif -%} + +{%- endfor -%} +{%- endfor -%} + +{{ release_notes.summary }}"; + + private const string ExpectedCustomHtml = @"

System

+
    +
  • {new} This is the second list item.
  • +
  • {fix} This is the third list item.
  • +
+

A little summary

"; + + private const string ExpectedAppVeyorData = @"{ ""name"": ""SemanticReleaseNotes"", ""value"": ""

A little summary

+

System

+
    +
  • {new} This is the second list item.
  • +
  • {fix} This is the third list item.
  • +
+ +"" }"; + } +} diff --git a/SemanticReleaseNotesParser.Tests/Properties/AssemblyInfo.cs b/SemanticReleaseNotesParser.Tests/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..a8401f4 --- /dev/null +++ b/SemanticReleaseNotesParser.Tests/Properties/AssemblyInfo.cs @@ -0,0 +1,36 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("SemanticReleaseNotesParser.Tests")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("SemanticReleaseNotesParser.Tests")] +[assembly: AssemblyCopyright("Copyright © 2015")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("6fc85324-7e4b-4856-9f34-b3ea4362733f")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/SemanticReleaseNotesParser.Tests/SemanticReleaseNotesParser.Tests.csproj b/SemanticReleaseNotesParser.Tests/SemanticReleaseNotesParser.Tests.csproj new file mode 100644 index 0000000..85fe97a --- /dev/null +++ b/SemanticReleaseNotesParser.Tests/SemanticReleaseNotesParser.Tests.csproj @@ -0,0 +1,97 @@ + + + + + + Debug + AnyCPU + {6FC85324-7E4B-4856-9F34-B3EA4362733F} + Library + Properties + SemanticReleaseNotesParser.Tests + SemanticReleaseNotesParser.Tests + v4.5 + 512 + 4fd55b04 + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + False + ..\packages\NDesk.Options.0.2.1\lib\NDesk.Options.dll + + + ..\packages\NSubstitute.1.8.1.0\lib\net45\NSubstitute.dll + + + + + ..\packages\System.IO.Abstractions.2.0.0.106\lib\net40\System.IO.Abstractions.dll + + + ..\packages\System.IO.Abstractions.TestingHelpers.2.0.0.106\lib\net40\System.IO.Abstractions.TestingHelpers.dll + + + + + + + + ..\packages\xunit.1.9.2\lib\net20\xunit.dll + + + + + + + + + + + + + + + + {982453c5-147d-435e-b70a-e25e6c2ee748} + SemanticReleaseNotesParser.Core + + + {d095be8a-58be-4722-ad79-151191c1243d} + SemanticReleaseNotesParser + + + + + + + + + This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. + + + + + \ No newline at end of file diff --git a/SemanticReleaseNotesParser.Tests/packages.config b/SemanticReleaseNotesParser.Tests/packages.config new file mode 100644 index 0000000..0b9eba7 --- /dev/null +++ b/SemanticReleaseNotesParser.Tests/packages.config @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/SemanticReleaseNotesParser.sln b/SemanticReleaseNotesParser.sln new file mode 100644 index 0000000..c04a0bc --- /dev/null +++ b/SemanticReleaseNotesParser.sln @@ -0,0 +1,40 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio 14 +VisualStudioVersion = 14.0.22512.0 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SemanticReleaseNotesParser", "SemanticReleaseNotesParser\SemanticReleaseNotesParser.csproj", "{D095BE8A-58BE-4722-AD79-151191C1243D}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SemanticReleaseNotesParser.Core", "SemanticReleaseNotesParser.Core\SemanticReleaseNotesParser.Core.csproj", "{982453C5-147D-435E-B70A-E25E6C2EE748}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SemanticReleaseNotesParser.Core.Tests", "SemanticReleaseNotesParser.Core.Tests\SemanticReleaseNotesParser.Core.Tests.csproj", "{90B46F40-8589-4665-AFF9-4CA01FAF03AD}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SemanticReleaseNotesParser.Tests", "SemanticReleaseNotesParser.Tests\SemanticReleaseNotesParser.Tests.csproj", "{6FC85324-7E4B-4856-9F34-B3EA4362733F}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {D095BE8A-58BE-4722-AD79-151191C1243D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D095BE8A-58BE-4722-AD79-151191C1243D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D095BE8A-58BE-4722-AD79-151191C1243D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D095BE8A-58BE-4722-AD79-151191C1243D}.Release|Any CPU.Build.0 = Release|Any CPU + {982453C5-147D-435E-B70A-E25E6C2EE748}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {982453C5-147D-435E-B70A-E25E6C2EE748}.Debug|Any CPU.Build.0 = Debug|Any CPU + {982453C5-147D-435E-B70A-E25E6C2EE748}.Release|Any CPU.ActiveCfg = Release|Any CPU + {982453C5-147D-435E-B70A-E25E6C2EE748}.Release|Any CPU.Build.0 = Release|Any CPU + {90B46F40-8589-4665-AFF9-4CA01FAF03AD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {90B46F40-8589-4665-AFF9-4CA01FAF03AD}.Debug|Any CPU.Build.0 = Debug|Any CPU + {90B46F40-8589-4665-AFF9-4CA01FAF03AD}.Release|Any CPU.ActiveCfg = Release|Any CPU + {90B46F40-8589-4665-AFF9-4CA01FAF03AD}.Release|Any CPU.Build.0 = Release|Any CPU + {6FC85324-7E4B-4856-9F34-B3EA4362733F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6FC85324-7E4B-4856-9F34-B3EA4362733F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6FC85324-7E4B-4856-9F34-B3EA4362733F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6FC85324-7E4B-4856-9F34-B3EA4362733F}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection +EndGlobal diff --git a/SemanticReleaseNotesParser/Abstractions/EnvironmentWrapper.cs b/SemanticReleaseNotesParser/Abstractions/EnvironmentWrapper.cs new file mode 100644 index 0000000..cbb291f --- /dev/null +++ b/SemanticReleaseNotesParser/Abstractions/EnvironmentWrapper.cs @@ -0,0 +1,24 @@ +using System; +using System.Diagnostics.CodeAnalysis; + +namespace SemanticReleaseNotesParser.Abstractions +{ + [ExcludeFromCodeCoverage] + internal class EnvironmentWrapper : IEnvironment + { + public string GetEnvironmentVariable(string variable) + { + return Environment.GetEnvironmentVariable(variable); + } + + public void SetEnvironmentVariable(string variable, string value) + { + Environment.SetEnvironmentVariable(variable, value); + } + + public void Exit(int exitCode) + { + Environment.Exit(exitCode); + } + } +} diff --git a/SemanticReleaseNotesParser/Abstractions/IEnvironment.cs b/SemanticReleaseNotesParser/Abstractions/IEnvironment.cs new file mode 100644 index 0000000..65d72d1 --- /dev/null +++ b/SemanticReleaseNotesParser/Abstractions/IEnvironment.cs @@ -0,0 +1,11 @@ +namespace SemanticReleaseNotesParser.Abstractions +{ + internal interface IEnvironment + { + string GetEnvironmentVariable(string variable); + + void SetEnvironmentVariable(string variable, string value); + + void Exit(int exitCode); + } +} diff --git a/SemanticReleaseNotesParser/Abstractions/IWebClient.cs b/SemanticReleaseNotesParser/Abstractions/IWebClient.cs new file mode 100644 index 0000000..35a45ef --- /dev/null +++ b/SemanticReleaseNotesParser/Abstractions/IWebClient.cs @@ -0,0 +1,9 @@ +using System; + +namespace SemanticReleaseNotesParser.Abstractions +{ + internal interface IWebClient : IDisposable + { + byte[] UploadData(string address, string method, byte[] data); + } +} diff --git a/SemanticReleaseNotesParser/Abstractions/IWebClientFactory.cs b/SemanticReleaseNotesParser/Abstractions/IWebClientFactory.cs new file mode 100644 index 0000000..ca1de4d --- /dev/null +++ b/SemanticReleaseNotesParser/Abstractions/IWebClientFactory.cs @@ -0,0 +1,7 @@ +namespace SemanticReleaseNotesParser.Abstractions +{ + internal interface IWebClientFactory + { + IWebClient Create(string baseAddress); + } +} diff --git a/SemanticReleaseNotesParser/Abstractions/WebClientFactory.cs b/SemanticReleaseNotesParser/Abstractions/WebClientFactory.cs new file mode 100644 index 0000000..899f47f --- /dev/null +++ b/SemanticReleaseNotesParser/Abstractions/WebClientFactory.cs @@ -0,0 +1,13 @@ +using System.Diagnostics.CodeAnalysis; + +namespace SemanticReleaseNotesParser.Abstractions +{ + [ExcludeFromCodeCoverage] + internal class WebClientFactory : IWebClientFactory + { + public IWebClient Create(string baseAddress) + { + return new WebClientWrapper(baseAddress); + } + } +} diff --git a/SemanticReleaseNotesParser/Abstractions/WebClientWrapper.cs b/SemanticReleaseNotesParser/Abstractions/WebClientWrapper.cs new file mode 100644 index 0000000..35fb6a0 --- /dev/null +++ b/SemanticReleaseNotesParser/Abstractions/WebClientWrapper.cs @@ -0,0 +1,16 @@ +using System.Diagnostics.CodeAnalysis; +using System.Net; + +namespace SemanticReleaseNotesParser.Abstractions +{ + [ExcludeFromCodeCoverage] + internal sealed class WebClientWrapper : WebClient, IWebClient + { + public WebClientWrapper(string baseAddress) + { + this.BaseAddress = baseAddress; + this.Headers["Accept"] = "application/json"; + this.Headers["Content-type"] = "application/json"; + } + } +} diff --git a/SemanticReleaseNotesParser/Arguments.cs b/SemanticReleaseNotesParser/Arguments.cs new file mode 100644 index 0000000..1fa3c2c --- /dev/null +++ b/SemanticReleaseNotesParser/Arguments.cs @@ -0,0 +1,51 @@ +using NDesk.Options; +using SemanticReleaseNotesParser.Core; + +namespace SemanticReleaseNotesParser +{ + internal class Arguments : OptionSet + { + private const string DefaultReleaseNotesPath = "ReleaseNotes.md"; + + public string ReleaseNotesPath { get; private set; } + + public string ResultFilePath { get; private set; } + + public OutputType OutputType { get; private set; } + + public string TemplatePath { get; private set; } + + public bool Debug { get; private set; } + + public bool Help { get; private set; } + + public OutputFormat OutputFormat { get; private set; } + + public Arguments() + { + Add("r|releasenotes=", "Release notes file path to parse (default: ReleaseNotes.md)", r => ReleaseNotesPath = r); + Add("o|outputfile=", "Path of the resulting file (default: ReleaseNotes.html", o => ResultFilePath = o); + Add("t|outputtype=", "Type of output [file|environment] (default: file)", t => OutputType = t); + Add("f|outputformat=", "Format of the resulting file [Html|Markdown] (default: Html)", f => OutputFormat = f); + Add("template=", "Path of the liquid template file to format the result", t => TemplatePath = t); + Add("debug", "Debug mode, more messages are logged", d => Debug = true); + Add("h|?|help", "Help", h => Help = true); + + ReleaseNotesPath = DefaultReleaseNotesPath; + ResultFilePath = "ReleaseNotes.html"; + } + + public static Arguments ParseArguments(string[] args) + { + var arguments = new Arguments(); + var additionalArguments = arguments.Parse(args); + + if (arguments.ReleaseNotesPath == DefaultReleaseNotesPath && additionalArguments.Count > 0 && !string.IsNullOrEmpty(additionalArguments[0])) + { + arguments.ReleaseNotesPath = additionalArguments[0]; + } + + return arguments; + } + } +} diff --git a/SemanticReleaseNotesParser/BuildServers/AppVeyor.cs b/SemanticReleaseNotesParser/BuildServers/AppVeyor.cs new file mode 100644 index 0000000..2b7b5e8 --- /dev/null +++ b/SemanticReleaseNotesParser/BuildServers/AppVeyor.cs @@ -0,0 +1,35 @@ +using SemanticReleaseNotesParser.Abstractions; +using System.Text; + +namespace SemanticReleaseNotesParser.BuildServers +{ + internal sealed class AppVeyor : IBuildServer + { + private const string SetEnvironmentVariableRequest = "{{ \"name\": \"{0}\", \"value\": \"{1}\" }}"; + + private readonly IEnvironment _environment; + private readonly IWebClientFactory _webClientFactory; + private readonly string _appVeyorAPIUrl; + + public AppVeyor(IEnvironment environment, IWebClientFactory webClientFactory) + { + _environment = environment; + _webClientFactory = webClientFactory; + _appVeyorAPIUrl = _environment.GetEnvironmentVariable("APPVEYOR_API_URL"); + } + + public bool CanApplyToCurrentContext() + { + return !string.IsNullOrEmpty(_environment.GetEnvironmentVariable("APPVEYOR")); + } + + public void SetEnvironmentVariable(string variable, string value) + { + using (var webClient = _webClientFactory.Create(_appVeyorAPIUrl)) + { + webClient.UploadData("api/build/variables", "POST", Encoding.UTF8.GetBytes(string.Format(SetEnvironmentVariableRequest, variable, value))); + Logger.Info("Adding AppVeyor environment variable: {0}.", variable); + } + } + } +} diff --git a/SemanticReleaseNotesParser/BuildServers/IBuildServer.cs b/SemanticReleaseNotesParser/BuildServers/IBuildServer.cs new file mode 100644 index 0000000..86800bc --- /dev/null +++ b/SemanticReleaseNotesParser/BuildServers/IBuildServer.cs @@ -0,0 +1,9 @@ +namespace SemanticReleaseNotesParser.BuildServers +{ + internal interface IBuildServer + { + bool CanApplyToCurrentContext(); + + void SetEnvironmentVariable(string variable, string value); + } +} diff --git a/SemanticReleaseNotesParser/BuildServers/LocalBuildServer.cs b/SemanticReleaseNotesParser/BuildServers/LocalBuildServer.cs new file mode 100644 index 0000000..8b5c70a --- /dev/null +++ b/SemanticReleaseNotesParser/BuildServers/LocalBuildServer.cs @@ -0,0 +1,25 @@ +using SemanticReleaseNotesParser.Abstractions; + +namespace SemanticReleaseNotesParser.BuildServers +{ + internal sealed class LocalBuildServer : IBuildServer + { + private readonly IEnvironment _environment; + + public LocalBuildServer(IEnvironment environment) + { + _environment = environment; + } + + public bool CanApplyToCurrentContext() + { + return true; + } + + public void SetEnvironmentVariable(string variable, string value) + { + _environment.SetEnvironmentVariable(variable, value); + Logger.Info("Adding local environment variable: {0}.", variable); + } + } +} diff --git a/SemanticReleaseNotesParser/Logger.cs b/SemanticReleaseNotesParser/Logger.cs new file mode 100644 index 0000000..0143dd6 --- /dev/null +++ b/SemanticReleaseNotesParser/Logger.cs @@ -0,0 +1,57 @@ +using System; +using System.Collections.Generic; +using System.IO; + +namespace SemanticReleaseNotesParser +{ + internal static class Logger + { + private static readonly List DefaultCategories = new List { "info", "error" }; + private static List categories = new List(DefaultCategories); + private static TextWriter writer; + + public static void SetWriter(TextWriter textWriter) + { + writer = textWriter; + } + + public static void AddCategory(string category) + { + categories.Add(category); + } + + public static void Debug(string message, params object[] args) + { + Write(message, "debug", args); + } + + public static void Info(string message, params object[] args) + { + Write(message, "info", args); + } + + public static void Error(string message, params object[] args) + { + Write(message, "error", args); + } + + private static void Write(string message, string category, params object[] args) + { + if (writer == null) + { + throw new Exception("The writer must be set with the 'SetWriter' method first."); + } + + if (categories.Contains(category)) + { + writer.WriteLine(message, args); + } + } + + internal static void Reset() + { + writer = null; + categories = new List(DefaultCategories); + } + } +} diff --git a/SemanticReleaseNotesParser/OutputType.cs b/SemanticReleaseNotesParser/OutputType.cs new file mode 100644 index 0000000..37d6911 --- /dev/null +++ b/SemanticReleaseNotesParser/OutputType.cs @@ -0,0 +1,8 @@ +namespace SemanticReleaseNotesParser +{ + internal enum OutputType + { + File, + Environment + } +} diff --git a/SemanticReleaseNotesParser/Program.cs b/SemanticReleaseNotesParser/Program.cs new file mode 100644 index 0000000..a7b7254 --- /dev/null +++ b/SemanticReleaseNotesParser/Program.cs @@ -0,0 +1,133 @@ +using SemanticReleaseNotesParser.Abstractions; +using SemanticReleaseNotesParser.BuildServers; +using SemanticReleaseNotesParser.Core; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.IO.Abstractions; +using System.Linq; + +namespace SemanticReleaseNotesParser +{ + internal class Program + { + internal static IFileSystem FileSystem { get; set; } + + internal static IEnvironment Environment { get; set; } + + internal static IWebClientFactory WebClientFactory { get; set; } + + internal static void Main(string[] args) + { + var exitCode = Run(args); + if (Debugger.IsAttached) + { + Console.ReadKey(); + } + Environment.Exit(exitCode); + } + + private static int Run(string[] args) + { + try + { + SetDefaults(); + + var output = Console.Out; + Logger.SetWriter(output); + + // Arguments parsing + var arguments = Arguments.ParseArguments(args); + if (arguments.Help) + { + arguments.WriteOptionDescriptions(output); + return 1; + } + + // Handle debug + if (arguments.Debug) + { + Logger.AddCategory("debug"); + } + + if (!FileSystem.File.Exists(arguments.ReleaseNotesPath)) + { + Logger.Error("Release notes file '{0}' does not exists", arguments.ReleaseNotesPath); + return 1; + } + + // Parsing + var releaseNotes = Core.SemanticReleaseNotesParser.Parse(FileSystem.File.OpenText(arguments.ReleaseNotesPath)); + + // Formatting + string template = null; + if (!string.IsNullOrEmpty(arguments.TemplatePath)) + { + if (!FileSystem.File.Exists(arguments.TemplatePath)) + { + Logger.Error("Template file '{0}' does not exists", arguments.TemplatePath); + return 1; + } + else + { + template = FileSystem.File.ReadAllText(arguments.TemplatePath); + } + } + + var formatterSettings = new SemanticReleaseNotesFormatterSettings { OutputFormat = arguments.OutputFormat, LiquidTemplate = template }; + string formattedReleaseNotes = SemanticReleaseNotesFormatter.Format(releaseNotes, formatterSettings); + + // Select output + if (arguments.OutputType == OutputType.File) + { + Logger.Debug("File output"); + FileSystem.File.WriteAllText(arguments.ResultFilePath, formattedReleaseNotes); + } + else if (arguments.OutputType == OutputType.Environment) + { + Logger.Debug("Environment output"); + var buildServer = GetApplicableBuildServer(); + Logger.Debug("Build server selected: {0}", buildServer.GetType().Name); + buildServer.SetEnvironmentVariable("SemanticReleaseNotes", formattedReleaseNotes); + } + + return 0; + } + catch (Exception exception) + { + var error = string.Format("An unexpected error occurred:\r\n{0}", exception); + Logger.Error(error); + return 1; + } + } + + [ExcludeFromCodeCoverage] + private static void SetDefaults() + { + if (FileSystem == null) + { + FileSystem = new FileSystem(); + } + + if (Environment == null) + { + Environment = new EnvironmentWrapper(); + } + + if (WebClientFactory == null) + { + WebClientFactory = new WebClientFactory(); + } + } + + private static IBuildServer GetApplicableBuildServer() + { + return new List + { + new AppVeyor(Environment, WebClientFactory), + new LocalBuildServer(Environment) + }.First(bs => bs.CanApplyToCurrentContext()); + } + } +} diff --git a/SemanticReleaseNotesParser/Properties/AssemblyInfo.cs b/SemanticReleaseNotesParser/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..3525e29 --- /dev/null +++ b/SemanticReleaseNotesParser/Properties/AssemblyInfo.cs @@ -0,0 +1,38 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("SemanticReleaseNotesParser")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("SemanticReleaseNotesParser")] +[assembly: AssemblyCopyright("Copyright © 2015")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("f73dfda0-e7ae-4b92-bc2a-aa9bb9555916")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] +[assembly: InternalsVisibleTo("SemanticReleaseNotesParser.Tests")] +[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2")] diff --git a/SemanticReleaseNotesParser/SemanticReleaseNotesParser.csproj b/SemanticReleaseNotesParser/SemanticReleaseNotesParser.csproj new file mode 100644 index 0000000..e4e852c --- /dev/null +++ b/SemanticReleaseNotesParser/SemanticReleaseNotesParser.csproj @@ -0,0 +1,106 @@ + + + + + Debug + AnyCPU + {D095BE8A-58BE-4722-AD79-151191C1243D} + Exe + Properties + SemanticReleaseNotesParser + SemanticReleaseNotesParser + v4.0 + 512 + + + AnyCPU + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + true + + + AnyCPU + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + False + ..\packages\DotLiquid.1.8.0\lib\NET40\DotLiquid.dll + + + ..\packages\NDesk.Options.0.2.1\lib\NDesk.Options.dll + + + + + False + ..\packages\System.IO.Abstractions.2.0.0.106\lib\net40\System.IO.Abstractions.dll + + + + + + + + + + + + + + + component + + + + + + + + + + + + + {982453c5-147d-435e-b70a-e25e6c2ee748} + SemanticReleaseNotesParser.Core + + + + + + + + + + This project references NuGet package(s) that are missing on this computer. Enable NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. + + + + + + + + + + + + + + \ No newline at end of file diff --git a/SemanticReleaseNotesParser/packages.config b/SemanticReleaseNotesParser/packages.config new file mode 100644 index 0000000..501bd67 --- /dev/null +++ b/SemanticReleaseNotesParser/packages.config @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/appveyor.yml b/appveyor.yml new file mode 100644 index 0000000..6558b2d --- /dev/null +++ b/appveyor.yml @@ -0,0 +1,40 @@ +configuration: Release + +install: +- GitVersion /output buildserver /UpdateAssemblyInfo true +- choco install opencover -source https://nuget.org/api/v2/ +- choco install coveralls.io -source https://nuget.org/api/v2/ +- choco install resharper-clt + +cache: +- packages -> **\packages.config +- C:\ProgramData\chocolatey\bin\OpenCover.Console.exe -> appveyor.yml +- C:\ProgramData\chocolatey\bin\coveralls.net.exe -> appveyor.yml +- C:\ProgramData\chocolatey\bin\inspectcode.exe -> appveyor.yml +- C:\ProgramData\chocolatey\lib -> appveyor.yml + +environment: + COVERALLS_REPO_TOKEN: + secure: S45Gg5pKn0LcC70m7IjzcmPVcuqsdK+Uiq8kUorOfQDM1OM+7pjlcaD0eomEJrqz + +build: + project: SemanticReleaseNotesParser.sln + verbosity: minimal + +after_build: +- inspectcode /o="inspectcodereport.xml" "SemanticReleaseNotesParser.sln" +#- NVika\bin\Release\NVika buildserver "inspectcodereport.xml" --debug --includesource + +test_script: +- OpenCover.Console.exe -register:user -filter:"+[SemanticReleaseNotesParser.Core]*" -excludebyattribute:*.ExcludeFromCodeCoverage* -target:"xunit.console.clr4.exe" -targetargs:"""SemanticReleaseNotesParser.Core.Tests\bin\Release\SemanticReleaseNotesParser.Core.Tests.dll"" /noshadow /appveyor" -output:coverage.xml -returntargetcode +- coveralls.net --opencover coverage.xml +- OpenCover.Console.exe -register:user -filter:"+[SemanticReleaseNotesParser]*" -excludebyattribute:*.ExcludeFromCodeCoverage* -target:"xunit.console.clr4.exe" -targetargs:"""SemanticReleaseNotesParser.Tests\bin\Release\SemanticReleaseNotesParser.Tests.dll"" /noshadow /appveyor" -output:coverage.xml -returntargetcode +- coveralls.net --opencover coverage.xml + +artifacts: +- path: inspectcodereport.xml +- path: coverage.xml +- path: NVika\bin\Release\SemanticReleaseNotesParser.exe.CodeAnalysisLog.xml + name: SemanticReleaseNotesParser.exe.CodeAnalysisLog.xml +- path: NVika\bin\Release\SemanticReleaseNotesParser.Core..dll.CodeAnalysisLog.xml + name: SemanticReleaseNotesParser.Core.dll.CodeAnalysisLog.xml