From 0efed34c570e5ef1c1a8bc653e78e55613a32393 Mon Sep 17 00:00:00 2001 From: Chet Husk Date: Sun, 23 Jan 2022 12:17:30 -0600 Subject: [PATCH 1/3] propose version bumps from unreleased changes --- .editorconfig | 13 ++ src/Ionide.KeepAChangelog.Tasks/Library.fs | 57 +++-- .../build/Ionide.KeepAChangelog.Tasks.props | 5 + .../build/Ionide.KeepAChangelog.Tasks.targets | 20 +- src/Ionide.KeepAChangelog/Library.fs | 215 +++++++++++------- 5 files changed, 206 insertions(+), 104 deletions(-) create mode 100644 .editorconfig diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..6ceefb0 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,13 @@ +root = true + +# All files +[*] +indent_style = space +trim_trailing_whitespace = true + +# Xml files +[*.xml] +indent_size = 2 + +[.fs] +indent_size = 2 \ No newline at end of file diff --git a/src/Ionide.KeepAChangelog.Tasks/Library.fs b/src/Ionide.KeepAChangelog.Tasks/Library.fs index 4fc8095..9089de0 100644 --- a/src/Ionide.KeepAChangelog.Tasks/Library.fs +++ b/src/Ionide.KeepAChangelog.Tasks/Library.fs @@ -4,19 +4,16 @@ open Microsoft.Build.Utilities open Microsoft.Build.Framework open System.IO open Ionide.KeepAChangelog -open Ionide.KeepAChangelog.Domain open System.Linq +open SemVersion +open System module Util = - let mapReleaseInfo (version: SemVersion.SemanticVersion) (date: System.DateTime) (item: ITaskItem) : ITaskItem = + let mapReleaseInfo (version: SemanticVersion) (date: DateTime) (item: ITaskItem) : ITaskItem = item.ItemSpec <- string version item.SetMetadata("Date", date.ToString("yyyy-MM-dd")) item - let mapUnreleasedInfo (item: ITaskItem) : ITaskItem = - item.ItemSpec <- "Unreleased" - item - let allReleaseNotesFor (data: ChangelogData) = let section name items = match items with @@ -24,18 +21,17 @@ module Util = | items -> $"### {name}" :: items @ [ "" ] String.concat - System.Environment.NewLine + Environment.NewLine ([ yield! section "Added" data.Added yield! section "Changed" data.Changed yield! section "Deprecated" data.Deprecated yield! section "Removed" data.Removed yield! section "Fixed" data.Fixed yield! section "Security" data.Security - for KeyValue(heading, lines) in data.Custom do - yield! section heading lines ]) + for KeyValue (heading, lines) in data.Custom do + yield! section heading lines ]) - let stitch items = - String.concat System.Environment.NewLine items + let stitch items = String.concat Environment.NewLine items let mapChangelogData (data: ChangelogData) (item: ITaskItem) : ITaskItem = item.SetMetadata("Added", stitch data.Added) @@ -44,10 +40,27 @@ module Util = item.SetMetadata("Removed", stitch data.Removed) item.SetMetadata("Fixed", stitch data.Fixed) item.SetMetadata("Security", stitch data.Security) - for (KeyValue(heading, lines)) in data.Custom do + + for (KeyValue (heading, lines)) in data.Custom do item.SetMetadata(heading, stitch lines) + item + let mapUnreleasedInfo changelogs (item: ITaskItem) : ITaskItem = + match Promote.fromUnreleased changelogs with + | None -> + item.ItemSpec <- "Unreleased" + + changelogs.Unreleased + |> Option.map (fun d -> mapChangelogData d item) + |> Option.defaultValue item + | Some (unreleasedVersion, releaseDate, data) -> + let item = mapReleaseInfo unreleasedVersion releaseDate item + + data + |> Option.map (fun d -> mapChangelogData d item) + |> Option.defaultValue item + type ParseChangelogs() = inherit Task() @@ -57,6 +70,9 @@ type ParseChangelogs() = [] member val UnreleasedChangelog: ITaskItem = null with get, set + [] + member val UnreleasedReleaseNotes: string = null with get, set + [] member val CurrentReleaseChangelog: ITaskItem = null with get, set @@ -66,6 +82,7 @@ type ParseChangelogs() = [] member val LatestReleaseNotes: string = null with get, set + override this.Execute() : bool = let file = this.ChangelogFile |> FileInfo @@ -77,10 +94,8 @@ type ParseChangelogs() = | Ok changelogs -> changelogs.Unreleased |> Option.iter (fun unreleased -> - this.UnreleasedChangelog <- - TaskItem() - |> Util.mapChangelogData unreleased - |> Util.mapUnreleasedInfo) + this.UnreleasedChangelog <- TaskItem() |> Util.mapUnreleasedInfo changelogs + this.UnreleasedReleaseNotes <- Util.allReleaseNotesFor unreleased) let sortedReleases = // have to use LINQ here because List.sortBy* require IComparable, which @@ -92,8 +107,10 @@ type ParseChangelogs() = |> Seq.map (fun (version, date, data) -> TaskItem() |> Util.mapReleaseInfo version date - |> fun d -> match data with Some data -> Util.mapChangelogData data d | None -> d - ) + |> fun d -> + match data with + | Some data -> Util.mapChangelogData data d + | None -> d) |> Seq.toArray this.AllReleasedChangelogs <- items @@ -103,9 +120,7 @@ type ParseChangelogs() = |> Seq.tryHead |> Option.iter (fun (version, date, data) -> data - |> Option.iter (fun data -> - this.LatestReleaseNotes <- Util.allReleaseNotesFor data) - ) + |> Option.iter (fun data -> this.LatestReleaseNotes <- Util.allReleaseNotesFor data)) true | Error (formatted, msg) -> diff --git a/src/Ionide.KeepAChangelog.Tasks/build/Ionide.KeepAChangelog.Tasks.props b/src/Ionide.KeepAChangelog.Tasks/build/Ionide.KeepAChangelog.Tasks.props index 4f5fed2..14176f8 100644 --- a/src/Ionide.KeepAChangelog.Tasks/build/Ionide.KeepAChangelog.Tasks.props +++ b/src/Ionide.KeepAChangelog.Tasks/build/Ionide.KeepAChangelog.Tasks.props @@ -1,5 +1,10 @@ + CHANGELOG.md + + true + + true \ No newline at end of file diff --git a/src/Ionide.KeepAChangelog.Tasks/build/Ionide.KeepAChangelog.Tasks.targets b/src/Ionide.KeepAChangelog.Tasks/build/Ionide.KeepAChangelog.Tasks.targets index e63d812..92ef8c9 100644 --- a/src/Ionide.KeepAChangelog.Tasks/build/Ionide.KeepAChangelog.Tasks.targets +++ b/src/Ionide.KeepAChangelog.Tasks/build/Ionide.KeepAChangelog.Tasks.targets @@ -7,14 +7,15 @@ $(PrepareForBuildDependsOn) - + - + @@ -24,15 +25,24 @@ + %(CurrentReleaseChangelog.Identity) %(CurrentReleaseChangelog.Identity) @(LatestReleaseNotes) + <_ReleaseDate>%(CurrentReleaseChangelog.Date) + + + + %(UnreleasedChangelog.Identity) + %(UnreleasedChangelog.Identity) + @(UnreleasedReleaseNotes) + <_ReleaseDate>%(UnreleasedChangelog.Date) - - + + <_Parameter1>BuildDate - <_Parameter2>%(CurrentReleaseChangelog.Date) + <_Parameter2>@(_ReleaseDate) diff --git a/src/Ionide.KeepAChangelog/Library.fs b/src/Ionide.KeepAChangelog/Library.fs index ae144b3..2c9b881 100644 --- a/src/Ionide.KeepAChangelog/Library.fs +++ b/src/Ionide.KeepAChangelog/Library.fs @@ -1,40 +1,94 @@ namespace Ionide.KeepAChangelog -module Domain = +open SemVersion +open System + +type ChangelogData = + { Added: string list + Changed: string list + Deprecated: string list + Removed: string list + Fixed: string list + Security: string list + Custom: Map } + static member Default = + { Added = [] + Changed = [] + Deprecated = [] + Removed = [] + Fixed = [] + Security = [] + Custom = Map.empty } + +type Changelogs = + { Unreleased: ChangelogData option + Releases: (SemanticVersion * DateTime * ChangelogData option) list } + +module Promote = open SemVersion - open System - - // TODO: a changelog entry may have a description? - type ChangelogData = - { Added: string list - Changed: string list - Deprecated: string list - Removed: string list - Fixed: string list - Security: string list - Custom: Map} - static member Default = - { Added = [] - Changed = [] - Deprecated = [] - Removed = [] - Fixed = [] - Security = [] - Custom = Map.empty } - - type Changelogs = - { Unreleased: ChangelogData option - Releases: (SemanticVersion * DateTime * ChangelogData option) list } -module Parser = + type private SemVerBump = + | Major + | Minor + | Patch + + let inline (|NonEmpty|_|) xs = + match xs with + | [] -> None + | _ -> Some() + + /// the prerelease segment is just a count of the individual changes + let revisionNumber (c: ChangelogData) = + c.Added.Length + + c.Changed.Length + + c.Deprecated.Length + + c.Fixed.Length + + c.Removed.Length + + c.Security.Length + + (c.Custom.Values |> Seq.sumBy (fun l -> l.Length)) + + // TODO: expand this logic later to allow for customization of the bump types based on change content? + let private determineBump (c: ChangelogData) : SemVerBump = + match c.Removed, c.Added with + | NonEmpty, _ -> Major + | _, NonEmpty -> Minor + | _, _ -> Patch + + /// for unreleased changes, bump the version an assign a prerelease part based on the number of changes + let private bumpVersion (ver: SemanticVersion) bumpType revisions = + let prereleasePart = $"beta.{revisions}" + + match bumpType with + | Major -> SemanticVersion(ver.Major.Value + 1, Nullable(), Nullable(), prerelease = prereleasePart) + | Minor -> SemanticVersion(ver.Major, ver.Minor.Value + 1, Nullable(), prerelease = prereleasePart) + | Patch -> SemanticVersion(ver.Major, ver.Minor, ver.Patch.Value + 1, prerelease = prereleasePart) + + /// given a changelog, determine the version to promote to from the unreleased changes, if any. + /// + /// The version bump algoritm is as follows: + /// * removals require a major bump + /// * additions require a minor bump + /// * all other changes require a patch bump + /// + let fromUnreleased (c: Changelogs) = + c.Unreleased + |> Option.bind (fun unreleased -> + match c.Releases |> List.tryHead with + | None -> None + | Some (lastVersion, _, releaseData) -> + let bumpType = + releaseData + |> Option.map determineBump + |> Option.defaultValue Patch + + let numberOfRevisions = revisionNumber unreleased + let newVersion = bumpVersion lastVersion bumpType numberOfRevisions + Some(newVersion, DateTime.Today, Some unreleased)) - open Domain +module Parser = open FParsec - open FParsec.CharParsers - open FParsec.Primitives open System.IO - open System.Collections.Generic type Parser<'t> = Parser<'t, unit> @@ -65,7 +119,7 @@ module Parser = fun stream -> let mutable found = false - stream.SkipCharsOrNewlinesUntilString(str, System.Int32.MaxValue, &found) + stream.SkipCharsOrNewlinesUntilString(str, Int32.MaxValue, &found) |> ignore Reply(()) @@ -80,18 +134,18 @@ module Parser = // but we also need to keep parsing next lines until // * we find a bullet, or // * we get an empty line - let firstLine = FParsec.CharParsers.restOfLine true + let firstLine = restOfLine true let followingLine = nextCharSatisfiesNot (fun c -> c = '\n' || c = '-' || c = '*') >>. spaces1 - >>. FParsec.CharParsers.restOfLine true + >>. restOfLine true let rest = opt (many1 (attempt followingLine)) - pipe2 firstLine rest (fun f rest -> + pipe2 firstLine rest (fun f rest -> match rest with - | None -> f + | None -> f | Some parts -> String.concat " " (f :: parts)) "line item" @@ -99,10 +153,9 @@ module Parser = let pCustomSection: Parser = let sectionName = - skipString "###" - >>. spaces1 - >>. restOfLine true // TODO: maybe not the whole line? - $"custom section header" + skipString "###" >>. spaces1 >>. restOfLine true // TODO: maybe not the whole line? + $"custom section header" + sectionName .>>. (many pEntry $"{sectionName} entries") .>> attempt (opt newline) @@ -125,15 +178,28 @@ module Parser = let pOrEmptyList p = opt (attempt p) let pSections: Parser ChangelogData> = - choice [ - attempt (pAdded |>> fun x data -> { data with Added = x }) - attempt (pChanged |>> fun x data -> { data with Changed = x }) - attempt (pRemoved |>> fun x data -> { data with Removed = x }) - attempt (pDeprecated |>> fun x data -> { data with Deprecated = x }) - attempt (pFixed |>> fun x data -> { data with Fixed = x }) - attempt (pSecurity |>> fun x data -> { data with Security = x }) - attempt (many1 pCustomSection |>> fun x data -> { data with Custom = Map.ofList x }) - ] + choice [ attempt (pAdded |>> fun x data -> { data with Added = x }) + attempt ( + pChanged + |>> fun x data -> { data with Changed = x } + ) + attempt ( + pRemoved + |>> fun x data -> { data with Removed = x } + ) + attempt ( + pDeprecated + |>> fun x data -> { data with Deprecated = x } + ) + attempt (pFixed |>> fun x data -> { data with Fixed = x }) + attempt ( + pSecurity + |>> fun x data -> { data with Security = x } + ) + attempt ( + many1 pCustomSection + |>> fun x data -> { data with Custom = Map.ofList x } + ) ] let pData: Parser = many1 pSections @@ -153,13 +219,15 @@ module Parser = let pUnreleased: Parser = let unreleased = skipString "Unreleased" - let name = attempt ( - skipString "##" - >>. spaces1 - >>. (mdUrl unreleased <|> unreleased) - .>> skipRestOfLine true - "Unreleased label" - ) + + let name = + attempt ( + skipString "##" + >>. spaces1 + >>. (mdUrl unreleased <|> unreleased) + .>> skipRestOfLine true + "Unreleased label" + ) name >>. opt (many newline) >>. opt pData "Unreleased version section" @@ -178,30 +246,24 @@ module Parser = |>> fun text -> SemVersion.SemanticVersion.Parse text let pDate: Parser<_> = - let pYear = - pint32 - |> attempt + let pYear = pint32 |> attempt - let pMonth = - pint32 - |> attempt + let pMonth = pint32 |> attempt - let pDay = - pint32 - |> attempt + let pDay = pint32 |> attempt - let ymdDashes = + let ymdDashes = let dash = pchar '-' pipe5 pYear dash pMonth dash pDay (fun y _ m _ d -> System.DateTime(y, m, d)) - let dmyDots = + let dmyDots = let dot = pchar '.' pipe5 pDay dot pMonth dot pYear (fun d _ m _ y -> System.DateTime(y, m, d)) attempt dmyDots <|> ymdDashes - + let pVersion = mdUrl pSemver <|> pSemver @@ -215,18 +277,15 @@ module Parser = let pChangeLogs: Parser = let unreleased = pUnreleased - |>> fun unreleased -> - match unreleased with - | None -> None - | Some u when u = ChangelogData.Default -> None - | Some unreleased -> Some unreleased - pipe3 - pHeader - (attempt (opt unreleased)) - (attempt (many pRelease)) - (fun header unreleased releases -> - { Unreleased = defaultArg unreleased None - Releases = releases }) + |>> fun unreleased -> + match unreleased with + | None -> None + | Some u when u = ChangelogData.Default -> None + | Some unreleased -> Some unreleased + + pipe3 pHeader (attempt (opt unreleased)) (attempt (many pRelease)) (fun header unreleased releases -> + { Unreleased = defaultArg unreleased None + Releases = releases }) let parseChangeLog (file: FileInfo) = match From d2b6f4485e07bf0b050afbda4489575e6f58288c Mon Sep 17 00:00:00 2001 From: Chet Husk Date: Sun, 23 Jan 2022 12:25:26 -0600 Subject: [PATCH 2/3] add release notes for the new props and data structures --- README.md | 24 ++--- test/Ionide.KeepAChangelog.Test/Program.fs | 119 ++++++++++++--------- 2 files changed, 78 insertions(+), 65 deletions(-) diff --git a/README.md b/README.md index e316510..b4d0bbb 100644 --- a/README.md +++ b/README.md @@ -33,7 +33,7 @@ It might be helpful to see how this library can help you. Imagine you have a pr and a CHANGELOG.md file like this: ```md -# Changelog +# Changelog ## 1.0.0 - 2022-01-14 @@ -71,18 +71,25 @@ If your changelog has multiple versions, the latest one will be used. There's really only one property that matters for these targets, and that's `ChangelogFile`. This needs to point to the Changelog file you want to read, but it defaults to `CHANGELOG.md` in the root of a given project in case you want to adhere to defaults. +| Property | Type | Default Value | Description | +| - | - | - | - | +| ChangelogFile | string | CHANGELOG.md | Points to the changelog file to parse. Note that the default value is set to the _project_ root by default, so a repository-wide changelog would require this property be set to a different value, for example in a Directory.Build.props file | +| GenerateAssemblyBuildDateAttribute | boolean | true | If set, an assembly metadata attribute named "BuildDate" will be generated with the date (YYYY-MM-DD) of the parsed release. | +| GenerateVersionForUnreleasedChanges | boolean | true | If set, the assembly/package version and release notes will be set from Unreleased changes, if any are present. | + ## API When the task runs, it writes several output items and properties: |Name|Type|Description| |----|----|-----------| -| UnreleasedChangelog | UnreleasedChangelogData option | If present, there was an 'Unreleased' section in the Changelog. This structure will contain the sections present. | +| UnreleasedChangelog | ReleaseChangelogData option | If present, there was an 'Unreleased' section in the Changelog. This structure will contain the sections present, as well as an auto-incremented version number for this release. | +| UnreleasedReleaseNotes | string option | If present, contains the concatenated list of all Changelog sections for the Unreleased section of the Changelog. This is a convenience property so that you don't have to String.Join all the lines in the `ReleaseChangelogData` structure yourself! | | CurrentReleaseChangelog | ReleaseChangelogData option | If present, there was at least one released logged in the Changelog. This structure will contain the details of each one. | | AllReleasedChangelogs | ReleaseChangelogData list | Contains the ordered list of all released in the ChangelogFile, descending. | -| LatestReleaseNotes | string option | If present, contains the concatenated list of all Changelog sections for the latest release. This is a convenience property so that you don't have to String.Join all the lines in the `ReleaseChangelogData` yourself! | +| LatestReleaseNotes | string option | If present, contains the concatenated list of all Changelog sections for the latest release. This is a convenience property so that you don't have to String.Join all the lines in the `ReleaseChangelogData` structure yourself! | -### ChangelogData +### ReleaseChangelogData This TaskItem has metadata for each of the known sections of a Changelog: @@ -95,13 +102,6 @@ This TaskItem has metadata for each of the known sections of a Changelog: In each case, the value of the metadata is the newline-concatenated list of all of the Changelog Entries for that section. -### UnreleasedChangelogData - -This structure is a `ChangelogData` with an `Identity` of `"Unreleased"`. - -### ReleaseChangelogData - -This structure is the same as `ChangelogData`, but it contains two more items of metadata: - +In addition, * the `Identity` of the `TaskItem` is the Semantic Version of the release * the `Date` of the `TaskItem` is the `YYYY-MM-DD`-formatted date of the release \ No newline at end of file diff --git a/test/Ionide.KeepAChangelog.Test/Program.fs b/test/Ionide.KeepAChangelog.Test/Program.fs index 47cc4e5..f99c297 100644 --- a/test/Ionide.KeepAChangelog.Test/Program.fs +++ b/test/Ionide.KeepAChangelog.Test/Program.fs @@ -3,7 +3,7 @@ open System open SemVersion open Expecto -open Ionide.KeepAChangelog.Domain +open Ionide.KeepAChangelog let singleRelease = """## [1.0.0] - 2017-06-20 @@ -18,13 +18,14 @@ let singleRelease = """ -let singleReleaseExpected = - (SemanticVersion.Parse "1.0.0", DateTime(2017, 06, 20), Some { - ChangelogData.Default with - Added = ["- A"] - Changed = ["- B"] - Removed = ["- C"] - }) +let singleReleaseExpected = + (SemanticVersion.Parse "1.0.0", + DateTime(2017, 6, 20), + Some + { ChangelogData.Default with + Added = [ "- A" ] + Changed = [ "- B" ] + Removed = [ "- C" ] }) let keepAChangelog = """# Changelog @@ -53,14 +54,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 """ -let keepAChangelogExpected: Changelogs = - { - Unreleased = None - Releases = [ - singleReleaseExpected - SemanticVersion.Parse("0.3.0"), DateTime(2015, 12, 03), Some { ChangelogData.Default with Added = ["- A";"- B";"- C"]} - ] - } +let keepAChangelogExpected: Changelogs = + { Unreleased = None + Releases = + [ singleReleaseExpected + SemanticVersion.Parse("0.3.0"), + DateTime(2015, 12, 3), + Some { ChangelogData.Default with Added = [ "- A"; "- B"; "- C" ] } ] } let header = """# Changelog @@ -81,7 +81,8 @@ let headerAndUnreleased = header + emptyUnreleased let headerAndUnreleasedAndRelease = header + emptyUnreleased + singleRelease let headerAndUnreleasedAndReleaseExpected = None, singleReleaseExpected -let sample1Release = """## [0.3.1] - 8.1.2022 +let sample1Release = + """## [0.3.1] - 8.1.2022 ### Added @@ -89,10 +90,13 @@ let sample1Release = """## [0.3.1] - 8.1.2022 """ -let sample1ReleaseExpected = - SemanticVersion.Parse "0.3.1", DateTime(2022, 1, 8), Some { ChangelogData.Default with Added = ["- Add XmlDocs to the generated package"] } +let sample1ReleaseExpected = + SemanticVersion.Parse "0.3.1", + DateTime(2022, 1, 8), + Some { ChangelogData.Default with Added = [ "- Add XmlDocs to the generated package" ] } -let sample = """# Changelog +let sample = + """# Changelog All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), @@ -124,15 +128,25 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * Initial implementation """ -let sampleExpected: Changelogs = { - Unreleased = None - Releases = [ - SemanticVersion.Parse "0.3.1", DateTime(2022, 1, 8), Some { ChangelogData.Default with Added = ["* Add XmlDocs to the generated package"] } - SemanticVersion.Parse "0.3.0", DateTime(2021, 11, 23), Some { ChangelogData.Default with Added = ["* Expose client `CodeAction` caps as CodeActionClientCapabilities. (by @razzmatazz)"; "* Map CodeAction.IsPreferred & CodeAction.Disabled props. (by @razzmatazz)"] } - SemanticVersion.Parse "0.2.0", DateTime(2021, 11, 17), Some { ChangelogData.Default with Added = ["* Add support for `codeAction/resolve` (by @razzmatazz)"] } - SemanticVersion.Parse "0.1.1", DateTime(2021, 11, 15), Some { ChangelogData.Default with Added = ["* Initial implementation"] } - ] -} +let sampleExpected: Changelogs = + { Unreleased = None + Releases = + [ SemanticVersion.Parse "0.3.1", + DateTime(2022, 1, 8), + Some { ChangelogData.Default with Added = [ "* Add XmlDocs to the generated package" ] } + SemanticVersion.Parse "0.3.0", + DateTime(2021, 11, 23), + Some + { ChangelogData.Default with + Added = + [ "* Expose client `CodeAction` caps as CodeActionClientCapabilities. (by @razzmatazz)" + "* Map CodeAction.IsPreferred & CodeAction.Disabled props. (by @razzmatazz)" ] } + SemanticVersion.Parse "0.2.0", + DateTime(2021, 11, 17), + Some { ChangelogData.Default with Added = [ "* Add support for `codeAction/resolve` (by @razzmatazz)" ] } + SemanticVersion.Parse "0.1.1", + DateTime(2021, 11, 15), + Some { ChangelogData.Default with Added = [ "* Initial implementation" ] } ] } open FParsec open FParsec.Primitives @@ -141,32 +155,31 @@ let runSuccess label p text expected = test $"parsing {label}" { match FParsec.CharParsers.run p text with - | FParsec.CharParsers.Success (r, _, _) -> - Expect.equal r expected "Should have produced expected value" - | FParsec.CharParsers.Failure (m, _, _) -> - failwithf "%A" m + | FParsec.CharParsers.Success (r, _, _) -> Expect.equal r expected "Should have produced expected value" + | FParsec.CharParsers.Failure (m, _, _) -> failwithf "%A" m } [] -let tests = testList "parsing examples" [ - runSuccess "line entry" Parser.pEntry "- A" "- A" - runSuccess "header" Parser.pHeader header () - runSuccess "unreleased" Parser.pUnreleased emptyUnreleased None - runSuccess "header and unreleased" (Parser.pHeader >>. Parser.pUnreleased) headerAndUnreleased None - runSuccess "release" Parser.pRelease singleRelease singleReleaseExpected - runSuccess "sample 1 release" Parser.pRelease sample1Release sample1ReleaseExpected - runSuccess - "header and unreleased and released" - (Parser.pHeader >>. Parser.pUnreleased - .>>. Parser.pRelease) - headerAndUnreleasedAndRelease - headerAndUnreleasedAndReleaseExpected - - runSuccess "keepachangelog" Parser.pChangeLogs keepAChangelog keepAChangelogExpected - - runSuccess "lsp changelog" Parser.pChangeLogs sample sampleExpected -] +let tests = + testList + "parsing examples" + [ runSuccess "line entry" Parser.pEntry "- A" "- A" + runSuccess "header" Parser.pHeader header () + runSuccess "unreleased" Parser.pUnreleased emptyUnreleased None + runSuccess "header and unreleased" (Parser.pHeader >>. Parser.pUnreleased) headerAndUnreleased None + runSuccess "release" Parser.pRelease singleRelease singleReleaseExpected + runSuccess "sample 1 release" Parser.pRelease sample1Release sample1ReleaseExpected + runSuccess + "header and unreleased and released" + (Parser.pHeader >>. Parser.pUnreleased + .>>. Parser.pRelease) + headerAndUnreleasedAndRelease + headerAndUnreleasedAndReleaseExpected + + runSuccess "keepachangelog" Parser.pChangeLogs keepAChangelog keepAChangelogExpected + + runSuccess "lsp changelog" Parser.pChangeLogs sample sampleExpected ] [] -let main argv = - runTestsWithCLIArgs Seq.empty argv tests \ No newline at end of file +let main argv = + runTestsWithCLIArgs Seq.empty argv tests From ce3812be77487ca063fc5595ad1b637728fdd1eb Mon Sep 17 00:00:00 2001 From: Chet Husk Date: Sun, 23 Jan 2022 14:46:08 -0600 Subject: [PATCH 3/3] initial tests of the task itself --- src/Ionide.KeepAChangelog.Tasks/Library.fs | 2 +- .../Ionide.KeepAChangelog.Test.fsproj | 5 + .../Ionide.KeepAChangelog.Test/ParserTests.fs | 180 ++++++++++++++++ test/Ionide.KeepAChangelog.Test/Program.fs | 181 +---------------- test/Ionide.KeepAChangelog.Test/TaskTests.fs | 192 ++++++++++++++++++ 5 files changed, 380 insertions(+), 180 deletions(-) create mode 100644 test/Ionide.KeepAChangelog.Test/ParserTests.fs create mode 100644 test/Ionide.KeepAChangelog.Test/TaskTests.fs diff --git a/src/Ionide.KeepAChangelog.Tasks/Library.fs b/src/Ionide.KeepAChangelog.Tasks/Library.fs index 9089de0..311052a 100644 --- a/src/Ionide.KeepAChangelog.Tasks/Library.fs +++ b/src/Ionide.KeepAChangelog.Tasks/Library.fs @@ -1,4 +1,4 @@ -namespace KeepAChangelog.Tasks +namespace Ionide.KeepAChangelog.Tasks open Microsoft.Build.Utilities open Microsoft.Build.Framework diff --git a/test/Ionide.KeepAChangelog.Test/Ionide.KeepAChangelog.Test.fsproj b/test/Ionide.KeepAChangelog.Test/Ionide.KeepAChangelog.Test.fsproj index 2b2316a..b3dafb8 100644 --- a/test/Ionide.KeepAChangelog.Test/Ionide.KeepAChangelog.Test.fsproj +++ b/test/Ionide.KeepAChangelog.Test/Ionide.KeepAChangelog.Test.fsproj @@ -10,15 +10,20 @@ + + + + + diff --git a/test/Ionide.KeepAChangelog.Test/ParserTests.fs b/test/Ionide.KeepAChangelog.Test/ParserTests.fs new file mode 100644 index 0000000..d7508e5 --- /dev/null +++ b/test/Ionide.KeepAChangelog.Test/ParserTests.fs @@ -0,0 +1,180 @@ +module ParserTests + +open Ionide.KeepAChangelog +open System +open SemVersion +open Expecto + +let singleRelease = + """## [1.0.0] - 2017-06-20 +### Added +- A + +### Changed +- B + +### Removed +- C + +""" + +let singleReleaseExpected = + (SemanticVersion.Parse "1.0.0", + DateTime(2017, 6, 20), + Some + { ChangelogData.Default with + Added = [ "- A" ] + Changed = [ "- B" ] + Removed = [ "- C" ] }) + +let keepAChangelog = + """# Changelog +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [Unreleased] + +## [1.0.0] - 2017-06-20 +### Added +- A + +### Changed +- B + +### Removed +- C + +## [0.3.0] - 2015-12-03 +### Added +- A +- B +- C + +""" + +let keepAChangelogExpected: Changelogs = + { Unreleased = None + Releases = + [ singleReleaseExpected + SemanticVersion.Parse("0.3.0"), + DateTime(2015, 12, 3), + Some { ChangelogData.Default with Added = [ "- A"; "- B"; "- C" ] } ] } + +let header = + """# Changelog +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +""" + +let emptyUnreleased = + """## [Unreleased] + +""" + +let headerAndUnreleased = header + emptyUnreleased + +let headerAndUnreleasedAndRelease = header + emptyUnreleased + singleRelease +let headerAndUnreleasedAndReleaseExpected = None, singleReleaseExpected + +let sample1Release = + """## [0.3.1] - 8.1.2022 + +### Added + +- Add XmlDocs to the generated package + +""" + +let sample1ReleaseExpected = + SemanticVersion.Parse "0.3.1", + DateTime(2022, 1, 8), + Some { ChangelogData.Default with Added = [ "- Add XmlDocs to the generated package" ] } + +let sample = + """# Changelog +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [0.3.1] - 8.1.2022 + +### Added + +* Add XmlDocs to the generated package + +## [0.3.0] - 23.11.2021 + +### Added + +* Expose client `CodeAction` caps as CodeActionClientCapabilities. (by @razzmatazz) +* Map CodeAction.IsPreferred & CodeAction.Disabled props. (by @razzmatazz) + +## [0.2.0] - 17.11.2021 + +### Added + +* Add support for `codeAction/resolve` (by @razzmatazz) + +## [0.1.1] - 15.11.2021 + +### Added + +* Initial implementation +""" + +let sampleExpected: Changelogs = + { Unreleased = None + Releases = + [ SemanticVersion.Parse "0.3.1", + DateTime(2022, 1, 8), + Some { ChangelogData.Default with Added = [ "* Add XmlDocs to the generated package" ] } + SemanticVersion.Parse "0.3.0", + DateTime(2021, 11, 23), + Some + { ChangelogData.Default with + Added = + [ "* Expose client `CodeAction` caps as CodeActionClientCapabilities. (by @razzmatazz)" + "* Map CodeAction.IsPreferred & CodeAction.Disabled props. (by @razzmatazz)" ] } + SemanticVersion.Parse "0.2.0", + DateTime(2021, 11, 17), + Some { ChangelogData.Default with Added = [ "* Add support for `codeAction/resolve` (by @razzmatazz)" ] } + SemanticVersion.Parse "0.1.1", + DateTime(2021, 11, 15), + Some { ChangelogData.Default with Added = [ "* Initial implementation" ] } ] } + +open FParsec +open FParsec.Primitives + +let runSuccess label p text expected = + test $"parsing {label}" { + + match FParsec.CharParsers.run p text with + | FParsec.CharParsers.Success (r, _, _) -> Expect.equal r expected "Should have produced expected value" + | FParsec.CharParsers.Failure (m, _, _) -> failwithf "%A" m + } + +let tests = + testList + "parsing examples" + [ runSuccess "line entry" Parser.pEntry "- A" "- A" + runSuccess "header" Parser.pHeader header () + runSuccess "unreleased" Parser.pUnreleased emptyUnreleased None + runSuccess "header and unreleased" (Parser.pHeader >>. Parser.pUnreleased) headerAndUnreleased None + runSuccess "release" Parser.pRelease singleRelease singleReleaseExpected + runSuccess "sample 1 release" Parser.pRelease sample1Release sample1ReleaseExpected + runSuccess + "header and unreleased and released" + (Parser.pHeader >>. Parser.pUnreleased + .>>. Parser.pRelease) + headerAndUnreleasedAndRelease + headerAndUnreleasedAndReleaseExpected + + runSuccess "keepachangelog" Parser.pChangeLogs keepAChangelog keepAChangelogExpected + + runSuccess "lsp changelog" Parser.pChangeLogs sample sampleExpected ] diff --git a/test/Ionide.KeepAChangelog.Test/Program.fs b/test/Ionide.KeepAChangelog.Test/Program.fs index f99c297..bc6e7ed 100644 --- a/test/Ionide.KeepAChangelog.Test/Program.fs +++ b/test/Ionide.KeepAChangelog.Test/Program.fs @@ -1,184 +1,7 @@ -open Ionide.KeepAChangelog - -open System -open SemVersion -open Expecto -open Ionide.KeepAChangelog - -let singleRelease = - """## [1.0.0] - 2017-06-20 -### Added -- A - -### Changed -- B - -### Removed -- C - -""" - -let singleReleaseExpected = - (SemanticVersion.Parse "1.0.0", - DateTime(2017, 6, 20), - Some - { ChangelogData.Default with - Added = [ "- A" ] - Changed = [ "- B" ] - Removed = [ "- C" ] }) - -let keepAChangelog = - """# Changelog -All notable changes to this project will be documented in this file. - -The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), -and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). - -## [Unreleased] - -## [1.0.0] - 2017-06-20 -### Added -- A - -### Changed -- B - -### Removed -- C - -## [0.3.0] - 2015-12-03 -### Added -- A -- B -- C - -""" - -let keepAChangelogExpected: Changelogs = - { Unreleased = None - Releases = - [ singleReleaseExpected - SemanticVersion.Parse("0.3.0"), - DateTime(2015, 12, 3), - Some { ChangelogData.Default with Added = [ "- A"; "- B"; "- C" ] } ] } - -let header = - """# Changelog -All notable changes to this project will be documented in this file. - -The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), -and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). - -""" - -let emptyUnreleased = - """## [Unreleased] - -""" - -let headerAndUnreleased = header + emptyUnreleased - -let headerAndUnreleasedAndRelease = header + emptyUnreleased + singleRelease -let headerAndUnreleasedAndReleaseExpected = None, singleReleaseExpected - -let sample1Release = - """## [0.3.1] - 8.1.2022 - -### Added - -- Add XmlDocs to the generated package - -""" - -let sample1ReleaseExpected = - SemanticVersion.Parse "0.3.1", - DateTime(2022, 1, 8), - Some { ChangelogData.Default with Added = [ "- Add XmlDocs to the generated package" ] } - -let sample = - """# Changelog -All notable changes to this project will be documented in this file. - -The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), -and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). - -## [0.3.1] - 8.1.2022 - -### Added - -* Add XmlDocs to the generated package - -## [0.3.0] - 23.11.2021 - -### Added - -* Expose client `CodeAction` caps as CodeActionClientCapabilities. (by @razzmatazz) -* Map CodeAction.IsPreferred & CodeAction.Disabled props. (by @razzmatazz) - -## [0.2.0] - 17.11.2021 - -### Added - -* Add support for `codeAction/resolve` (by @razzmatazz) - -## [0.1.1] - 15.11.2021 - -### Added - -* Initial implementation -""" - -let sampleExpected: Changelogs = - { Unreleased = None - Releases = - [ SemanticVersion.Parse "0.3.1", - DateTime(2022, 1, 8), - Some { ChangelogData.Default with Added = [ "* Add XmlDocs to the generated package" ] } - SemanticVersion.Parse "0.3.0", - DateTime(2021, 11, 23), - Some - { ChangelogData.Default with - Added = - [ "* Expose client `CodeAction` caps as CodeActionClientCapabilities. (by @razzmatazz)" - "* Map CodeAction.IsPreferred & CodeAction.Disabled props. (by @razzmatazz)" ] } - SemanticVersion.Parse "0.2.0", - DateTime(2021, 11, 17), - Some { ChangelogData.Default with Added = [ "* Add support for `codeAction/resolve` (by @razzmatazz)" ] } - SemanticVersion.Parse "0.1.1", - DateTime(2021, 11, 15), - Some { ChangelogData.Default with Added = [ "* Initial implementation" ] } ] } - -open FParsec -open FParsec.Primitives - -let runSuccess label p text expected = - test $"parsing {label}" { - - match FParsec.CharParsers.run p text with - | FParsec.CharParsers.Success (r, _, _) -> Expect.equal r expected "Should have produced expected value" - | FParsec.CharParsers.Failure (m, _, _) -> failwithf "%A" m - } +open Expecto [] -let tests = - testList - "parsing examples" - [ runSuccess "line entry" Parser.pEntry "- A" "- A" - runSuccess "header" Parser.pHeader header () - runSuccess "unreleased" Parser.pUnreleased emptyUnreleased None - runSuccess "header and unreleased" (Parser.pHeader >>. Parser.pUnreleased) headerAndUnreleased None - runSuccess "release" Parser.pRelease singleRelease singleReleaseExpected - runSuccess "sample 1 release" Parser.pRelease sample1Release sample1ReleaseExpected - runSuccess - "header and unreleased and released" - (Parser.pHeader >>. Parser.pUnreleased - .>>. Parser.pRelease) - headerAndUnreleasedAndRelease - headerAndUnreleasedAndReleaseExpected - - runSuccess "keepachangelog" Parser.pChangeLogs keepAChangelog keepAChangelogExpected - - runSuccess "lsp changelog" Parser.pChangeLogs sample sampleExpected ] +let tests = testList "tests" [ ParserTests.tests; TaskTests.tests ] [] let main argv = diff --git a/test/Ionide.KeepAChangelog.Test/TaskTests.fs b/test/Ionide.KeepAChangelog.Test/TaskTests.fs new file mode 100644 index 0000000..32b8226 --- /dev/null +++ b/test/Ionide.KeepAChangelog.Test/TaskTests.fs @@ -0,0 +1,192 @@ +module TaskTests + +open Ionide.KeepAChangelog +open Expecto +open Ionide.KeepAChangelog.Tasks +open Microsoft.Build.Utilities +open Microsoft.Build.Framework +open Microsoft.Build +open System +open System.Collections +open System.Collections.Generic +open Microsoft.Build.Evaluation +open Microsoft.Build.Logging +open System.Collections.Concurrent +open System.Text + +type MockLogger() = + interface ILogger with + member val Verbosity = LoggerVerbosity.Normal with get, set + member val Parameters = null with get, set + member x.Initialize _ = () + member x.Shutdown() = () + + + +type MockEngine() = + let projectCollection = new ProjectCollection() + let globalProperties = Dictionary(StringComparer.OrdinalIgnoreCase) + let objectCache = ConcurrentDictionary() + let locker = obj () + let log = StringBuilder() + + member x.MockLogger = MockLogger() + member x.Log = log.ToString() + + interface IBuildEngine9 with + member val AllowFailureWithoutError = false with get, set + + override this.BuildProjectFile + ( + projectFileName: string, + targetNames: string [], + globalProperties: System.Collections.IDictionary, + targetOutputs: System.Collections.IDictionary, + toolsVersion: string + ) : bool = + let finalGlobalProperties = + new Dictionary(StringComparer.OrdinalIgnoreCase) + + // Finally, whatever global properties were passed into the task ... those are the final winners. + if globalProperties <> null then + for (newGlobalProperty: DictionaryEntry) in globalProperties |> Seq.cast do + finalGlobalProperties[(string) newGlobalProperty.Key] <- (string) newGlobalProperty.Value + + let project = + projectCollection.LoadProject(projectFileName, finalGlobalProperties, toolsVersion) + + let loggers: ILogger seq = [ this.MockLogger; ConsoleLogger() ] + + project.Build(targetNames, loggers) + + override this.BuildProjectFile + ( + projectFileName: string, + targetNames: string [], + globalProperties: System.Collections.IDictionary, + targetOutputs: System.Collections.IDictionary + ) : bool = + (this: IBuildEngine9) + .BuildProjectFile(projectFileName, targetNames, globalProperties, targetOutputs, null) + + override this.BuildProjectFilesInParallel + ( + projectFileNames: string [], + targetNames: string [], + globalProperties: System.Collections.IDictionary [], + removeGlobalProperties: System.Collections.Generic.IList [], + toolsVersion: string [], + returnTargetOutputs: bool + ) : BuildEngineResult = + failwith "b" + + override this.BuildProjectFilesInParallel + ( + projectFileNames: string [], + targetNames: string [], + globalProperties: System.Collections.IDictionary [], + targetOutputsPerProject: System.Collections.IDictionary [], + toolsVersion: string [], + useResultsCache: bool, + unloadProjectsOnCompletion: bool + ) : bool = + let includeTargetOutputs = targetOutputsPerProject <> null + + let result = + (this: IBuildEngine9) + .BuildProjectFilesInParallel( + projectFileNames, + targetNames, + globalProperties, + Array.zeroCreate projectFileNames.Length, + toolsVersion, + includeTargetOutputs + ) + + if includeTargetOutputs then + for i in 0 .. targetOutputsPerProject.Length do + if targetOutputsPerProject[i] <> null then + for (output: KeyValuePair) in result.TargetOutputsPerProject[i] do + targetOutputsPerProject[i] + .Add(output.Key, output.Value) + + result.Result + + override this.ColumnNumberOfTaskNode = 0 + override this.ContinueOnError = false + + override this.GetGlobalProperties() = globalProperties + + override this.GetRegisteredTaskObject(key: obj, lifetime: RegisteredTaskObjectLifetime) : obj = + match objectCache.TryGetValue(key) with + | true, obj -> obj + | false, o -> o + + override this.IsRunningMultipleNodes: bool = false + override this.LineNumberOfTaskNode: int = 0 + + override this.LogCustomEvent(e: CustomBuildEventArgs) : unit = + lock locker (fun _ -> log.AppendLine e.Message |> ignore) + + override this.LogErrorEvent(e: BuildErrorEventArgs) : unit = + lock locker (fun _ -> log.AppendLine e.Message |> ignore) + + override this.LogMessageEvent(e: BuildMessageEventArgs) : unit = + lock locker (fun _ -> log.AppendLine e.Message |> ignore) + + override this.LogTelemetry + ( + eventName: string, + properties: System.Collections.Generic.IDictionary + ) : unit = + let mutable message = $"""{eventName}:{Environment.NewLine}""" + + for (property: KeyValuePair) in properties do + message <- + message + + $"{property.Key}={property.Value};{Environment.NewLine}" + + lock locker (fun _ -> log.AppendLine message |> ignore) + + override this.LogWarningEvent(e: BuildWarningEventArgs) : unit = + lock locker (fun _ -> log.AppendLine e.Message |> ignore) + + override this.ProjectFileOfTaskNode: string = "" + override this.Reacquire() : unit = () + + override this.RegisterTaskObject + ( + key: obj, + obj: obj, + lifetime: RegisteredTaskObjectLifetime, + allowEarlyCollection: bool + ) : unit = + objectCache[key] <- obj + + override this.ReleaseCores(coresToRelease: int) : unit = () + override this.RequestCores(requestedCores: int) : int = requestedCores + override this.ShouldTreatWarningAsError(warningCode: string) : bool = false + + override this.UnregisterTaskObject(key: obj, lifetime: RegisteredTaskObjectLifetime) : obj = + match objectCache.TryRemove key with + | true, o -> o + | false, o -> o + + override this.Yield() : unit = () + +let tests = + testList + "MSBuild Task" + [ test "should fail without changelog file" { + let t = ParseChangelogs() + let engine = MockEngine() + t.BuildEngine <- engine + t.ChangelogFile <- "farts" + let result = t.Execute() + Expect.isFalse result "should have failed" + + Expect.stringContains + engine.Log + "farts could not be found" + "should have errored complaining about missing file" + } ]