diff --git a/src/app/FakeLib/ChangeLogHelper.fs b/src/app/FakeLib/ChangeLogHelper.fs new file mode 100644 index 00000000000..2c7cc985be4 --- /dev/null +++ b/src/app/FakeLib/ChangeLogHelper.fs @@ -0,0 +1,403 @@ +/// Contains helpers which allow to parse Change log text files. +/// These files have to be in a format as described on http://keepachangelog.com/en/0.3.0/ +/// +/// ## Sample +/// +/// let changeLogFile = "CHANGELOG.md" +/// let newVersion = "1.0.0" +/// +/// Target "AssemblyInfo" (fun _ -> +/// let changeLog = changeLogFile |> ChangeLogHelper.LoadChangeLog +/// CreateFSharpAssemblyInfo "src/Common/AssemblyInfo.fs" +/// [ Attribute.Title project +/// Attribute.Product project +/// Attribute.Description summary +/// Attribute.Version changeLog.LatestEntry.AssemblyVersion +/// Attribute.FileVersion changeLog.LatestEntry.AssemblyVersion] +/// ) +/// +/// Target "Promote Unreleased to new version" (fun _ -> +/// let newChangeLog = +/// changeLogFile +/// |> ChangeLogHelper.LoadChangeLog +/// |> ChangeLogHelper.PromoteUnreleased newVersion +/// |> ChangeLogHelper.SavceChangeLog changeLogFile +/// ) +module Fake.ChangeLogHelper + +open System +open System.Text.RegularExpressions +open Fake.AssemblyInfoFile + +let private trimLine = trimStartChars [|' '; '*'; '#'; '-'|] >> trimEndChars [|' '|] +let private trimLines lines = lines |> Seq.map trimLine |> Seq.toList + +type Change = + /// for new features + | Added of string + /// for changes in existing functionality + | Changed of string + /// for once-stable features removed in upcoming releases + | Deprecated of string + /// for deprecated features removed in this release + | Removed of string + /// for any bug fixes + | Fixed of string + /// to invite users to upgrade in case of vulnerabilities + | Security of string + /// Custom entry (Header, Description) + | Custom of string * string + + override x.ToString() = + match x with + | Added s -> sprintf "Added: %s" s + | Changed s -> sprintf "Changed: %s" s + | Deprecated s -> sprintf "Deprecated: %s" s + | Removed s -> sprintf "Removed: %s" s + | Fixed s -> sprintf "Fixed: %s" s + | Security s -> sprintf "Security: %s" s + | Custom (h, s) -> sprintf "%s: %s" h s + + static member New(header: string, line: string): Change = + let line = line |> trimLine + + match header |> trimLine |> toLower with + | "added" -> Added line + | "changed" -> Changed line + | "deprecated" -> Deprecated line + | "removed" -> Removed line + | "fixed" -> Fixed line + | "security" -> Security line + | _ -> Custom (header |> trimLine, line) + + +let private makeEntry change = + let bullet text = sprintf "- %s" text + + match change with + | Added c -> @"\n### Added", (bullet c) + | Changed c -> @"\n### Changed", (bullet c) + | Deprecated c -> @"\n### Deprecated", (bullet c) + | Removed c -> @"\n### Removed", (bullet c) + | Fixed c -> @"\n### Fixed", (bullet c) + | Security c -> @"\n### Security", (bullet c) + | Custom (h, c) -> (sprintf @"\n### %s" h), (bullet c) + +type ChangeLogEntry = + { /// the parsed Version + AssemblyVersion: string + /// the NuGet package version + NuGetVersion: string + /// Semantic version + SemVer: SemVerHelper.SemVerInfo + /// Release DateTime + Date: DateTime option + /// a descriptive text (after the header) + Description: string option + /// The parsed list of changes + Changes: Change list + /// True, if the entry was yanked + IsYanked: bool } + + override x.ToString() = + let header = + let isoDate = + match x.Date with + | Some d -> d.ToString(" - yyyy-MM-dd") + | None -> "" + + let yanked = if x.IsYanked then " [YANKED]" else "" + + sprintf "## %s%s%s\n" x.NuGetVersion isoDate yanked + + let description = + match x.Description with + | Some text -> sprintf @"\n%s\n" (text |> trim) + | None -> "" + + let changes = + x.Changes + |> List.map makeEntry + |> Seq.groupBy fst + |> Seq.map (fun (key, values) -> key :: (values |> Seq.map snd |> Seq.toList) |> separated @"\n") + |> separated @"\n" + + + (sprintf @"%s%s%s" header description changes).Replace(@"\n", Environment.NewLine).Trim() + + static member New(assemblyVersion, nugetVersion, date, description, changes, isYanked) = { + AssemblyVersion = assemblyVersion + NuGetVersion = nugetVersion + SemVer = SemVerHelper.parse nugetVersion + Date = date + Description = description + Changes = changes + IsYanked = isYanked } + + static member New(assemblyVersion, nugetVersion, changes) = ChangeLogEntry.New(assemblyVersion, nugetVersion, None, None, changes, false) + +type Unreleased = + { Description: string option + Changes: Change list } + + override x.ToString() = + let header = @"## Unreleased\n" + + let description = + match x.Description with + | Some text -> sprintf @"\n%s\n" (text |> trim) + | None -> "" + + let changes = + x.Changes + |> List.map makeEntry + |> Seq.groupBy fst + |> Seq.map (fun (key, values) -> key :: (values |> Seq.map snd |> Seq.toList) |> separated @"\n") + |> separated @"\n" + + (sprintf @"%s%s%s" header description changes).Replace(@"\n", Environment.NewLine).Trim() + + static member New(description, changes) = + match description with + | Some _ -> Some { Description = description; Changes = changes } + | None -> + match changes with + | [] -> None + | _ -> Some { Description = description; Changes = changes } + +let parseVersions = + let nugetRegex = getRegEx @"([0-9]+.)+[0-9]+(-[a-zA-Z]+\d*)?(.[0-9]+)?" + fun line -> + let assemblyVersion = assemblyVersionRegex.Match line + if not assemblyVersion.Success + then failwithf "Unable to parse valid Assembly version from change log(%s)." line + + let nugetVersion = nugetRegex.Match line + if not nugetVersion.Success + then failwithf "Unable to parse valid NuGet version from change log (%s)." line + assemblyVersion, nugetVersion + +type ChangeLog = + { /// the header line + Header: string + /// The description + Description: string option + /// The Unreleased section + Unreleased: Unreleased option + /// The change log entries + Entries: ChangeLogEntry list } + + /// the latest change log entry + member x.LatestEntry = x.Entries |> Seq.head + + static member New(header, description, unreleased, entries) = + { + Header = header + Description = description + Unreleased = unreleased + Entries = entries + } + + static member New(description, unreleased, entries) = + ChangeLog.New("Changelog", description, unreleased, entries) + + static member New(entries) = + ChangeLog.New(None, None, entries) + + member x.PromoteUnreleased(assemblyVersion: string, nugetVersion: string) : ChangeLog = + match x.Unreleased with + | None -> x + | Some u -> + let newEntry = ChangeLogEntry.New(assemblyVersion, nugetVersion, Some (System.DateTime.Today), u.Description, u.Changes, false) + + ChangeLog.New(x.Header, x.Description, None, newEntry :: x.Entries) + + member x.PromoteUnreleased(version: string) : ChangeLog = + let assemblyVersion, nugetVersion = version |> parseVersions + x.PromoteUnreleased(assemblyVersion.Value, nugetVersion.Value) + + override x.ToString() = + let description = + match x.Description with + | Some d -> sprintf @"\n%s\n" d + | _ -> "" + + let unreleased = + match x.Unreleased with + | Some u -> sprintf @"\n%s\n" (u.ToString()) + | _ -> "" + + let entries = + x.Entries + |> List.map (fun e -> sprintf @"\n%s\n" (e.ToString())) + |> separated @"" + + let header = + match x.Header |> trim with + | "" -> "Changelog" + | h -> h + + (sprintf @"# %s\n%s%s%s" header description unreleased entries).Replace(@"\n", Environment.NewLine) |> trim + +/// Parses a change log text and returns the change log. +/// +/// ## Parameters +/// - `data` - change log text +let parseChangeLog (data: seq) : ChangeLog = + let parseDate = + let dateRegex = getRegEx @"(19|20)\d\d([- /.])(0[1-9]|1[012]|[1-9])\2(0[1-9]|[12][0-9]|3[01]|[1-9])" + fun line -> + let possibleDate = dateRegex.Match line + match possibleDate.Success with + | false -> None + | true -> + match DateTime.TryParse possibleDate.Value with + | false, _ -> None + | true, x -> Some(x) + + let rec findFirstHeader accumulator lines = + match lines with + | [] -> accumulator |> List.filter (not << isNullOrWhiteSpace), [] + | line :: rest when "# " <* line -> accumulator, lines + | _ :: rest -> rest |> findFirstHeader accumulator + + let preHeaderLines, data = data |> Seq.toList |> findFirstHeader [] + + if preHeaderLines|> List.exists (not << isNullOrWhiteSpace) + then failwith "Invalid format: Changelog must begin with a Top level header!" + + match data |> List.filter (not << isNullOrWhiteSpace) with + | [] -> failwith "Empty change log file." + | header :: text -> + let isUnreleasedHeader line = "## " <* line && line.Contains("[Unreleased]") + let isBlockHeader line = "## " <* line && not <| line.Contains("[Unreleased]") + let isCategoryHeader line = "### " <* line + let isAnyHeader line = isBlockHeader line || isCategoryHeader line + + let rec findEnd headerPredicate accumulator lines = + match lines with + | [] -> accumulator,[] + | line :: rest when line |> headerPredicate -> accumulator, lines + | line :: rest -> rest |> findEnd headerPredicate (line :: accumulator) + + let rec findBlockEnd accumulator lines = findEnd isBlockHeader accumulator lines + + let rec findUnreleasedBlock (text: string list): (string list * string list) option = + match text with + | [] -> None + | h :: rest when h |> isUnreleasedHeader -> rest|> findBlockEnd [] |> Some + | _ :: rest -> findUnreleasedBlock rest + + let rec findNextChangesBlock text = + match text with + | [] -> None + | h :: rest when h |> isBlockHeader -> Some(h, rest |> findBlockEnd []) + | _ :: rest -> findNextChangesBlock rest + + let rec findNextCategoryBlock text = + let rec findCategoryEnd changes text = + match text with + | [] -> changes |> List.filter isNotNullOrEmpty,[] + | h :: rest when h |> isAnyHeader -> changes |> List.filter isNotNullOrEmpty, text + | h :: rest -> rest |> findCategoryEnd (h :: changes) + + match text with + | [] -> None + | h :: rest when h |> isCategoryHeader -> Some(h, findCategoryEnd [] rest) + | _ :: rest -> findNextCategoryBlock rest + + let rec categoryLoop (changes: Change list) (text: string list) : Change list = + match findNextCategoryBlock text with + | Some (header, (changeLines, rest)) -> + categoryLoop ((changeLines |> List.map trimLine |> List.filter isNotNullOrEmpty |> List.rev |> List.map (fun line -> Change.New(header,line))) |> List.append changes) rest + | None -> changes + + let rec loop changeLogEntries text = + match findNextChangesBlock text with + | Some (header, (changes, rest)) -> + let assemblyVer, nugetVer = parseVersions header + let date = parseDate header + let changeLines = categoryLoop [] (changes |> List.filter isNotNullOrEmpty |> List.rev) + let isYanked = (header |> toLower).Contains("[yanked]") + let description = + let descriptionLines, _ = + let isBlockOrCategoryHeader line = isCategoryHeader line || isBlockHeader line + findEnd isBlockOrCategoryHeader [] (changes |> Seq.toList |> List.rev) + + match descriptionLines |> List.rev with + | [] -> None + | lines -> lines |> List.map trim |> separated "\n" |> trim |> Some + + let newChangeLogEntry = ChangeLogEntry.New(assemblyVer.Value, nugetVer.Value, date, description, changeLines, isYanked) + loop (newChangeLogEntry::changeLogEntries) rest + | None -> changeLogEntries + + let description = + let descriptionLines, _ = + let isBlockOrUnreleasedHeader line = isUnreleasedHeader line || isBlockHeader line + findEnd isBlockOrUnreleasedHeader [] (data |> Seq.filter (not << (startsWith "# ")) |> Seq.toList) + + match descriptionLines |> List.rev with + | [] -> None + | lines -> lines |> List.map trim |> separated "\n" |> trim |> Some + + let unreleased = + match findUnreleasedBlock text with + | Some (changes, _) -> + let unreleasedChanges = categoryLoop [] (changes |> List.filter isNotNullOrEmpty |> List.rev) + + let description = + let descriptionLines, _ = + let isBlockOrCategoryHeader line = isCategoryHeader line || isBlockHeader line + findEnd isBlockOrCategoryHeader [] (changes |> Seq.toList |> List.rev) + + match descriptionLines |> List.rev with + | [] -> None + | lines -> lines |> List.map trim |> separated "\n" |> trim |> Some + + Unreleased.New(description, unreleasedChanges) + | _ -> None + + let entries = (loop [] text |> List.sortBy (fun x -> x.SemVer) |> List.rev) + + let header = + if "# " <* header then + header |> trimLine + else + match text |> List.filter (startsWith "# ") with + | h :: _ -> h |> trimLine + | _ -> "Changelog" + + ChangeLog.New(header, description, unreleased, entries) + + +/// Parses a Change log text file and returns the lastest change log. +/// +/// ## Parameters +/// - `fileName` - ChangeLog text file name +/// +/// ## Returns +/// The loaded change log (or throws an exception, if the change log could not be parsed) +let LoadChangeLog fileName = + System.IO.File.ReadLines fileName + |> parseChangeLog + +/// Saves a Change log to a text file. +/// +/// ## Parameters +/// - `fileName` - ChangeLog text file name +/// - `changeLog` - the change log data +let SaveChangeLog (fileName: string) (changeLog: ChangeLog) : unit = + System.IO.File.WriteAllText(fileName, changeLog.ToString()) + +/// Promotes the `Unreleased` section of a changelog +/// to a new change log entry with the given version +/// +/// ## Parameters +/// - `version` - The version (in NuGet-Version format, e.g. `3.13.4-alpha1.212` +/// - `changeLog` - The change log to promote +/// +/// ## Returns +/// The promoted change log +let PromoteUnreleased (version: string) (changeLog: ChangeLog) : ChangeLog = + changeLog.PromoteUnreleased(version) diff --git a/src/app/FakeLib/FakeLib.fsproj b/src/app/FakeLib/FakeLib.fsproj index 5052712abc9..255b231e882 100644 --- a/src/app/FakeLib/FakeLib.fsproj +++ b/src/app/FakeLib/FakeLib.fsproj @@ -190,6 +190,7 @@ + diff --git a/src/test/Test.FAKECore/ChangeLogHelperSpecs.cs b/src/test/Test.FAKECore/ChangeLogHelperSpecs.cs new file mode 100644 index 00000000000..ae6a3a5355a --- /dev/null +++ b/src/test/Test.FAKECore/ChangeLogHelperSpecs.cs @@ -0,0 +1,677 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Fake; +using Machine.Specifications; +using Microsoft.FSharp.Collections; + +namespace Test.FAKECore +{ + static class Changes + { + public static IEnumerable FromString(string data) + { + return data.Split(new[] { '\r', '\n' }, StringSplitOptions.None); + } + } + + public class when_parsing_change_log + { + private const string validData = @" +# Change Log +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](http://keepachangelog.com/) +and this project adheres to [Semantic Versioning](http://semver.org/). + +## [Unreleased] +### Added +- Line 1 Added (unreleased) +- Line 2 Added (unreleased) + +### Changed +- Line 1 Changed (unreleased) +- Line 2 Changed (unreleased) + +## [0.3.0] - 2015-12-03 + +This is a description +Description Line 2 + +### Added +- Line 1 0.3.0 Added +- Line 2 0.3.0 Added + +### Fixed +- Line 1 0.3.0 Fixed +- Line 2 0.3.0 Fixed + +## 0.2.0-beta1 - 2015-10-06 +### Changed +- Line 1 0.2.0-beta1 Changed +- Line 2 0.2.0-beta1 Changed + +[Unreleased]: https://github.com/olivierlacan/keep-a-changelog/compare/v0.3.0...HEAD +[0.3.0]: https://github.com/olivierlacan/keep-a-changelog/compare/v0.2.0...v0.3.0 +[0.2.0]: https://github.com/olivierlacan/keep-a-changelog/compare/v0.1.0...v0.2.0 +What’s the point of a change log?"; + + private static readonly ChangeLogHelper.ChangeLogEntry expected = ChangeLogHelper.ChangeLogEntry.New( + "0.3.0", + "0.3.0", + new Microsoft.FSharp.Core.FSharpOption(new DateTime(2015, 12, 03)), + new Microsoft.FSharp.Core.FSharpOption("This is a description\nDescription Line 2"), + new ChangeLogHelper.Change[] + { + ChangeLogHelper.Change.NewAdded("Line 1 0.3.0 Added"), + ChangeLogHelper.Change.NewAdded("Line 2 0.3.0 Added"), + ChangeLogHelper.Change.NewFixed("Line 1 0.3.0 Fixed"), + ChangeLogHelper.Change.NewFixed("Line 2 0.3.0 Fixed"), + }.ToFSharpList(), + false); + + It should_parse = + () => ChangeLogHelper.parseChangeLog(Changes.FromString(validData)).LatestEntry.ShouldEqual(expected); + + It should_parse_empty_changes = + () => ChangeLogHelper.parseChangeLog(Changes.FromString("# Change log\n\n## [0.3.0] - 2015-12-03")).LatestEntry + .ShouldEqual(ChangeLogHelper.ChangeLogEntry.New("0.3.0", "0.3.0", new Microsoft.FSharp.Core.FSharpOption(new DateTime(2015, 12, 03)), Microsoft.FSharp.Core.FSharpOption.None, FSharpList.Empty, false)); + + It should_throw_if_top_level_header_is_missing = + () => Catch.Exception(() => ChangeLogHelper.parseChangeLog(Changes.FromString(@"## [0.3.0] - 2015-12-03"))); + + It should_throw_if_top_level_header_is_not_first_non_empty_line = + () => Catch.Exception(() => ChangeLogHelper.parseChangeLog(Changes.FromString("FOO\n\n# Change log\n\n## [0.3.0] - 2015-12-03"))); + + It should_throw_on_empty_seq_input = + () => Catch.Exception(() => ChangeLogHelper.parseChangeLog(new string[] { })); + + It should_throw_on_null_input = + () => Catch.Exception(() => ChangeLogHelper.parseChangeLog(null)); + + It should_throw_on_single_empty_string_input = + () => Catch.Exception(() => ChangeLogHelper.parseChangeLog(new[] { "" })); + } + + public class when_parsing_custom_categories + { + const string validData = @" +# Changelog + +## [2.0.0] - 2013-12-15 +### Configuration +* The Configuration has changed + +"; + + private static readonly ChangeLogHelper.ChangeLogEntry expected = ChangeLogHelper.ChangeLogEntry.New( + "2.0.0", + "2.0.0", + new Microsoft.FSharp.Core.FSharpOption(new DateTime(2013, 12, 15)), + Microsoft.FSharp.Core.FSharpOption.None, + new ChangeLogHelper.Change[] + { + ChangeLogHelper.Change.NewCustom("Configuration", "The Configuration has changed"), + }.ToFSharpList(), + false); + + It should_parse = + () => ChangeLogHelper.parseChangeLog(Changes.FromString(validData)).LatestEntry.ShouldEqual(expected); + } + + public class when_parsing_yanked_entries + { + const string validData = @" +# Changelog + +## [2.0.0] - 2013-12-15 +### Added +* A + +## [1.0.0] - 2013-12-14 [YANKED] +### Added +* B + +"; + private static readonly ChangeLogHelper.ChangeLogEntry expected = ChangeLogHelper.ChangeLogEntry.New( + "2.0.0", + "2.0.0", + new Microsoft.FSharp.Core.FSharpOption(new DateTime(2013, 12, 15)), + Microsoft.FSharp.Core.FSharpOption.None, + new ChangeLogHelper.Change[] { ChangeLogHelper.Change.NewAdded("A") }.ToFSharpList(), + false); + + private static readonly ChangeLogHelper.ChangeLogEntry expected2 = ChangeLogHelper.ChangeLogEntry.New( + "1.0.0", + "1.0.0", + new Microsoft.FSharp.Core.FSharpOption(new DateTime(2013, 12, 14)), + Microsoft.FSharp.Core.FSharpOption.None, + new ChangeLogHelper.Change[] { ChangeLogHelper.Change.NewAdded("B") }.ToFSharpList(), + true); + + static ChangeLogHelper.ChangeLog _result; + + Because of = () => _result = ChangeLogHelper.parseChangeLog(Changes.FromString(validData)); + + It should_parse_the_first_entry_as_not_yanked = + () => _result.Entries.First().ShouldEqual(expected); + + It should_parse_the_second_entry_as_yanked = + () => _result.Entries.Skip(1).First().ShouldEqual(expected2); + } + + public class when_parsing_many_alpha_versions_in_change_log + { + const string input = @" +# Change Log +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](http://keepachangelog.com/) +and this project adheres to [Semantic Versioning](http://semver.org/). + +## [Unreleased] + +## [2.0.0-alpha] - 2013-12-15 +### Added +* A + +## 2.0.0-alpha2 - 2013-12-24 +### Added +* B + +## 2.0.0-alpha001 - 2013-12-15 +### Added +* A"; + + static readonly ChangeLogHelper.ChangeLogEntry expected = + ChangeLogHelper.ChangeLogEntry.New( + "2.0.0", + "2.0.0-alpha2", + new Microsoft.FSharp.Core.FSharpOption(new DateTime(2013, 12, 24)), + Microsoft.FSharp.Core.FSharpOption.None, + new ChangeLogHelper.Change[] { ChangeLogHelper.Change.NewAdded("B") }.ToFSharpList(), + false); + + It should_parse = + () => ChangeLogHelper.parseChangeLog(Changes.FromString(input)).LatestEntry.ShouldEqual(expected); + } + + public class when_parsing_all_changes + { + const string validData = @" + +# Change Log +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](http://keepachangelog.com/) +and this project adheres to [Semantic Versioning](http://semver.org/). + +## [Unreleased] + +This is a description +Description Line 2 + +### Added +- Unreleased added + +### Changed +- Unreleased changed + +### Deprecated +- Unreleased deprecated + +### Removed +- Unreleased removed + +### Fixed +- Unreleased fixed + +### Security +- Unreleased security + +## [1.1.9] - 2013-07-21 +### Changed +- Infer booleans for ints that only manifest 0 and 1. + +## [1.1.10] - 2013-09-12 +### Added +- Support for heterogeneous XML attributes. +- Make CsvFile re-entrant. +- Support for compressed HTTP responses. + +### Fixed +- Fix JSON conversion of 0 and 1 to booleans. +- Fix XmlProvider problems with nested elements and elements with same name in different namespaces. + +"; + + static readonly ChangeLogHelper.ChangeLogEntry expected = ChangeLogHelper.ChangeLogEntry.New( + "1.1.10", + "1.1.10", + new Microsoft.FSharp.Core.FSharpOption(new DateTime(2013, 09, 12)), + Microsoft.FSharp.Core.FSharpOption.None, + new ChangeLogHelper.Change[] + { + ChangeLogHelper.Change.NewAdded("Support for heterogeneous XML attributes."), + ChangeLogHelper.Change.NewAdded("Make CsvFile re-entrant."), + ChangeLogHelper.Change.NewAdded("Support for compressed HTTP responses."), + ChangeLogHelper.Change.NewFixed("Fix JSON conversion of 0 and 1 to booleans."), + ChangeLogHelper.Change.NewFixed("Fix XmlProvider problems with nested elements and elements with same name in different namespaces.") + }.ToFSharpList(), + false); + + static readonly ChangeLogHelper.ChangeLogEntry expected2 = ChangeLogHelper.ChangeLogEntry.New( + "1.1.9", + "1.1.9", + new Microsoft.FSharp.Core.FSharpOption(new DateTime(2013, 07, 21)), + Microsoft.FSharp.Core.FSharpOption.None, + new ChangeLogHelper.Change[] { ChangeLogHelper.Change.NewChanged("Infer booleans for ints that only manifest 0 and 1.") }.ToFSharpList(), + false); + + private static readonly Microsoft.FSharp.Core.FSharpOption expectedUnreleased = + ChangeLogHelper.Unreleased.New( + new Microsoft.FSharp.Core.FSharpOption("This is a description\nDescription Line 2"), + new ChangeLogHelper.Change[] + { + ChangeLogHelper.Change.NewAdded("Unreleased added"), + ChangeLogHelper.Change.NewChanged("Unreleased changed"), + ChangeLogHelper.Change.NewDeprecated("Unreleased deprecated"), + ChangeLogHelper.Change.NewRemoved("Unreleased removed"), + ChangeLogHelper.Change.NewFixed("Unreleased fixed"), + ChangeLogHelper.Change.NewSecurity("Unreleased security") + }.ToFSharpList()); + + static readonly Microsoft.FSharp.Core.FSharpOption expectedDescription = + new Microsoft.FSharp.Core.FSharpOption("All notable changes to this project will be documented in this file.\n\nThe format is based on [Keep a Changelog](http://keepachangelog.com/)\nand this project adheres to [Semantic Versioning](http://semver.org/)."); + + const string expectedHeader = "Change Log"; + + static ChangeLogHelper.ChangeLog _result; + + Because of = () => _result = ChangeLogHelper.parseChangeLog(Changes.FromString(validData)); + + It should_parse_the_correct_header_line = + () => _result.Header.ShouldEqual(expectedHeader); + + It should_find_both_entries = + () => _result.Entries.Length.ShouldEqual(2); + + It should_find_the_latest_entry = + () => _result.LatestEntry.ShouldEqual(expected); + + It should_find_the_first_entry = + () => _result.Entries.First().ShouldEqual(expected); + + It should_find_the_second_entry = + () => _result.Entries.Skip(1).First().ShouldEqual(expected2); + + It should_parse_the_unreleased_entries = + () => _result.Unreleased.ShouldEqual(expectedUnreleased); + + It should_parse_the_description = + () => _result.Description.ShouldEqual(expectedDescription); + } + + public class when_deserializing_a_ChangeLog_entry + { + private static string Normalize(string value) + { + return value + .Replace(Environment.NewLine, @"\n") + .Replace("\r\n", @"\n") + .Replace("\n\r", @"\n") + .Replace("\r", @"\n") + .Replace("\n", @"\n"); + } + + private It should_return_minimal_header_if_minimal_info_is_given = () => + { + var entry = ChangeLogHelper.ChangeLogEntry.New( + "0.3.0.1", + "0.3.0-beta1", + FSharpList.Empty); + + entry.ToString().ShouldEqual("## 0.3.0-beta1"); + }; + + private It should_return_header_with_date_if_date_is_given = () => + { + var entry = ChangeLogHelper.ChangeLogEntry.New( + "0.3.0.1", + "0.3.0-beta1", + new Microsoft.FSharp.Core.FSharpOption(new DateTime(2013, 09, 12)), + Microsoft.FSharp.Core.FSharpOption.None, + FSharpList.Empty, + false); + + entry.ToString().ShouldEqual("## 0.3.0-beta1 - 2013-09-12"); + }; + + private It should_return_header_with_date_and_YANKED_marker_if_date_is_given_and_IsYanked_is_true = () => + { + var entry = ChangeLogHelper.ChangeLogEntry.New( + "0.3.0.1", + "0.3.0-beta1", + new Microsoft.FSharp.Core.FSharpOption(new DateTime(2013, 09, 12)), + Microsoft.FSharp.Core.FSharpOption.None, + FSharpList.Empty, + true); + + entry.ToString().ShouldEqual("## 0.3.0-beta1 - 2013-09-12 [YANKED]"); + }; + + private It should_return_header_without_date_but_with_YANKED_marker_if_date_is_not_given_but_IsYanked_is_true = + () => + { + var entry = ChangeLogHelper.ChangeLogEntry.New( + "0.3.0.1", + "0.3.0-beta1", + Microsoft.FSharp.Core.FSharpOption.None, + Microsoft.FSharp.Core.FSharpOption.None, + FSharpList.Empty, + true); + + entry.ToString().ShouldEqual("## 0.3.0-beta1 [YANKED]"); + }; + + private It should_return_header_and_description_if_description_is_set = () => + { + var entry = ChangeLogHelper.ChangeLogEntry.New( + "0.3.0.1", + "0.3.0-beta1", + Microsoft.FSharp.Core.FSharpOption.None, + new Microsoft.FSharp.Core.FSharpOption("This is a description"), + FSharpList.Empty, + false); + + Normalize(entry.ToString()).ShouldEqual(Normalize("## 0.3.0-beta1\n\nThis is a description")); + }; + + private It should_return_changes_grouped_by_header = () => + { + var changes = + new ChangeLogHelper.Change[] + { + ChangeLogHelper.Change.NewAdded("added 1"), + ChangeLogHelper.Change.NewChanged("changed 1"), + ChangeLogHelper.Change.NewDeprecated("deprecated 1"), + ChangeLogHelper.Change.NewRemoved("removed 1"), + ChangeLogHelper.Change.NewFixed("fixed 1"), + ChangeLogHelper.Change.NewSecurity("security 1"), + ChangeLogHelper.Change.NewCustom("MyCustomHeader", "custom 1"), + ChangeLogHelper.Change.NewAdded("added 2"), + ChangeLogHelper.Change.NewChanged("changed 2"), + ChangeLogHelper.Change.NewDeprecated("deprecated 2"), + ChangeLogHelper.Change.NewRemoved("removed 2"), + ChangeLogHelper.Change.NewFixed("fixed 2"), + ChangeLogHelper.Change.NewSecurity("security 2"), + ChangeLogHelper.Change.NewCustom("MyCustomHeader", "custom 2") + } + .ToFSharpList(); + + var entry = ChangeLogHelper.ChangeLogEntry.New( + "0.3.0.1", + "0.3.0-beta1", + new Microsoft.FSharp.Core.FSharpOption(new DateTime(2013, 09, 12)), + new Microsoft.FSharp.Core.FSharpOption("This is a description"), + changes, + true); + + const string expected = +@"## 0.3.0-beta1 - 2013-09-12 [YANKED] + +This is a description + +### Added +- added 1 +- added 2 + +### Changed +- changed 1 +- changed 2 + +### Deprecated +- deprecated 1 +- deprecated 2 + +### Removed +- removed 1 +- removed 2 + +### Fixed +- fixed 1 +- fixed 2 + +### Security +- security 1 +- security 2 + +### MyCustomHeader +- custom 1 +- custom 2"; + + Normalize(entry.ToString()).ShouldEqual(Normalize(expected)); + }; + + private It should_have_correct_line_feeds_if_description_is_missing = () => + { + var entry = ChangeLogHelper.ChangeLogEntry.New( + "0.3.0.1", + "0.3.0-beta1", + new Microsoft.FSharp.Core.FSharpOption(new DateTime(2013, 09, 12)), + Microsoft.FSharp.Core.FSharpOption.None, + new ChangeLogHelper.Change[] { ChangeLogHelper.Change.NewAdded("added 1") }.ToFSharpList(), + true); + + const string expected = +@"## 0.3.0-beta1 - 2013-09-12 [YANKED] + +### Added +- added 1"; + + Normalize(entry.ToString()).ShouldEqual(Normalize(expected)); + }; + } + + public class when_deserializing_the_Unreleased_section + { + private static string Normalize(string value) + { + return value + .Replace(Environment.NewLine, @"\n") + .Replace("\r\n", @"\n") + .Replace("\n\r", @"\n") + .Replace("\r", @"\n") + .Replace("\n", @"\n"); + } + + private It should_return_header_and_description_if_description_is_set = () => + { + var entry = ChangeLogHelper.Unreleased.New( + new Microsoft.FSharp.Core.FSharpOption("This is a description"), + FSharpList.Empty); + + Normalize(entry.Value.ToString()).ShouldEqual(Normalize("## Unreleased\n\nThis is a description")); + }; + + private It should_return_changes_grouped_by_header = () => + { + var changes = new ChangeLogHelper.Change[] + { + ChangeLogHelper.Change.NewAdded("added 1"), + ChangeLogHelper.Change.NewChanged("changed 1"), + ChangeLogHelper.Change.NewDeprecated("deprecated 1"), + ChangeLogHelper.Change.NewRemoved("removed 1"), + ChangeLogHelper.Change.NewFixed("fixed 1"), + ChangeLogHelper.Change.NewSecurity("security 1"), + ChangeLogHelper.Change.NewCustom("MyCustomHeader", "custom 1"), + ChangeLogHelper.Change.NewAdded("added 2"), + ChangeLogHelper.Change.NewChanged("changed 2"), + ChangeLogHelper.Change.NewDeprecated("deprecated 2"), + ChangeLogHelper.Change.NewRemoved("removed 2"), + ChangeLogHelper.Change.NewFixed("fixed 2"), + ChangeLogHelper.Change.NewSecurity("security 2"), + ChangeLogHelper.Change.NewCustom("MyCustomHeader", "custom 2") + }.ToFSharpList(); + + var entry = ChangeLogHelper.Unreleased.New( + new Microsoft.FSharp.Core.FSharpOption("This is a description"), + changes); + + const string expected = +@"## Unreleased + +This is a description + +### Added +- added 1 +- added 2 + +### Changed +- changed 1 +- changed 2 + +### Deprecated +- deprecated 1 +- deprecated 2 + +### Removed +- removed 1 +- removed 2 + +### Fixed +- fixed 1 +- fixed 2 + +### Security +- security 1 +- security 2 + +### MyCustomHeader +- custom 1 +- custom 2"; + + Normalize(entry.Value.ToString()).ShouldEqual(Normalize(expected)); + }; + + private It should_have_correct_line_feeds_if_description_is_missing = () => + { + var entry = ChangeLogHelper.Unreleased.New( + Microsoft.FSharp.Core.FSharpOption.None, + new ChangeLogHelper.Change[] { ChangeLogHelper.Change.NewAdded("added 1") }.ToFSharpList()); + + const string expected = +@"## Unreleased + +### Added +- added 1"; + + Normalize(entry.Value.ToString()).ShouldEqual(Normalize(expected)); + }; + } + + public class when_deserializing_a_ChangeLog + { + private static string Normalize(string value) + { + return value + .Replace(Environment.NewLine, @"\n") + .Replace("\r\n", @"\n") + .Replace("\n\r", @"\n") + .Replace("\r", @"\n") + .Replace("\n", @"\n"); + } + + private static readonly ChangeLogHelper.ChangeLog changeLog = + ChangeLogHelper.ChangeLog.New( + "Changelog header", + new Microsoft.FSharp.Core.FSharpOption("This is a description"), + ChangeLogHelper.Unreleased.New( + new Microsoft.FSharp.Core.FSharpOption("Unreleased description"), + new ChangeLogHelper.Change[] + { + ChangeLogHelper.Change.NewAdded("Unreleased added") + }.ToFSharpList()), + new ChangeLogHelper.ChangeLogEntry[] + { + ChangeLogHelper.ChangeLogEntry.New( + "0.0.3", + "0.0.3", + new Microsoft.FSharp.Core.FSharpOption(new DateTime(2014, 12, 13)), + new Microsoft.FSharp.Core.FSharpOption("Description for 0.0.3"), + new ChangeLogHelper.Change[] { ChangeLogHelper.Change.NewFixed("Fixed in 0.0.3") }.ToFSharpList(), + false), + + ChangeLogHelper.ChangeLogEntry.New( + "0.0.2", + "0.0.2", + Microsoft.FSharp.Core.FSharpOption.None, + Microsoft.FSharp.Core.FSharpOption.None, + new ChangeLogHelper.Change[] { ChangeLogHelper.Change.NewDeprecated("Deprecated in 0.0.2") }.ToFSharpList(), + true) + }.ToFSharpList()); + + private const string expected = +@"# Changelog header + +This is a description + +## Unreleased + +Unreleased description + +### Added +- Unreleased added + +## 0.0.3 - 2014-12-13 + +Description for 0.0.3 + +### Fixed +- Fixed in 0.0.3 + +## 0.0.2 [YANKED] + +### Deprecated +- Deprecated in 0.0.2"; + + private It should_serialize_correctly = () => Normalize(changeLog.ToString()).ShouldEqual(Normalize(expected)); + } + + public class when_promoting_changes + { + private static readonly ChangeLogHelper.ChangeLog before = + ChangeLogHelper.ChangeLog.New( + "Changelog header", + new Microsoft.FSharp.Core.FSharpOption("This is a description"), + ChangeLogHelper.Unreleased.New( + new Microsoft.FSharp.Core.FSharpOption("Unreleased description"), + new ChangeLogHelper.Change[] + { + ChangeLogHelper.Change.NewAdded("Unreleased added") + }.ToFSharpList()), + FSharpList.Empty); + + private static readonly ChangeLogHelper.ChangeLog promoted = + ChangeLogHelper.ChangeLog.New( + "Changelog header", + new Microsoft.FSharp.Core.FSharpOption("This is a description"), + Microsoft.FSharp.Core.FSharpOption.None, + new ChangeLogHelper.ChangeLogEntry[] + { + ChangeLogHelper.ChangeLogEntry.New( + "0.0.3", + "0.0.3-beta1", + new Microsoft.FSharp.Core.FSharpOption(DateTime.Today), + new Microsoft.FSharp.Core.FSharpOption("Unreleased description"), + new ChangeLogHelper.Change[] { ChangeLogHelper.Change.NewAdded("Unreleased added") }.ToFSharpList(), + false) + }.ToFSharpList()); + + private It should_move_unreleased_entries_to_new_version_on_PromoteUnreleased = + () => before.PromoteUnreleased("0.0.3-beta1").ShouldEqual(promoted); + + private It should_not_change_on_PromoteUnreleased_if_there_is_no_Unreleased_section = + () => promoted.PromoteUnreleased("0.0.4-beta1").ShouldEqual(promoted); + } +} diff --git a/src/test/Test.FAKECore/Test.FAKECore.csproj b/src/test/Test.FAKECore/Test.FAKECore.csproj index 939eec1a3ed..220c0cc0a74 100644 --- a/src/test/Test.FAKECore/Test.FAKECore.csproj +++ b/src/test/Test.FAKECore/Test.FAKECore.csproj @@ -67,6 +67,7 @@ +