diff --git a/src/app/Fake.DotNet.Cli/DotNet.fs b/src/app/Fake.DotNet.Cli/DotNet.fs index 5963bb86605..f3771334e3f 100644 --- a/src/app/Fake.DotNet.Cli/DotNet.fs +++ b/src/app/Fake.DotNet.Cli/DotNet.fs @@ -1697,3 +1697,155 @@ module DotNet = nugetPush (fun _ -> param.WithPushParams { pushParams with PushTrials = pushParams.PushTrials - 1 }) nupkg else failwithf "dotnet nuget push failed with code %i" result.ExitCode + + /// the languages supported by new command + type NewLanguage = + | FSharp + | CSharp + | VisualBasic + + /// Convert the list option to string representation + override this.ToString() = + match this with + | FSharp -> "F#" + | CSharp -> "C#" + | VisualBasic -> "VB" + + /// dotnet new command options + type NewOptions = + { + /// Common tool options + Common: Options + // Displays a summary of what would happen if the given command line were run if it would result in a template creation. + DryRun: bool + // Forces content to be generated even if it would change existing files. + Force: bool + // Filters templates based on language and specifies the language of the template to create. + Language: NewLanguage + // The name for the created output. If no name is specified, the name of the current directory is used. + Name: string option + // Disables checking for template package updates when instantiating a template. + NoUpdateCheck: bool + // Location to place the generated output. The default is the current directory. + Output: string option + } + + /// Parameter default values. + static member Create() = { + Common = Options.Create() + DryRun = false + Force = false + Language = NewLanguage.FSharp + Name = None + NoUpdateCheck = false + Output = None + } + + /// dotnet new --install options + type TemplateInstallOptions = + { + /// Common tool options + Common: Options + Install: string + NugetSource: string option + } + + /// Parameter default values. + static member Create(packageOrSourceName) = { + Common = Options.Create() + Install = packageOrSourceName + NugetSource = None + } + + /// dotnet new --install options + type TemplateUninstallOptions = + { + /// Common tool options + Common: Options + Uninstall: string + } + + /// Parameter default values. + static member Create(packageOrSourceName) = { + Common = { Options.Create() with RedirectOutput = true } + Uninstall = packageOrSourceName + } + + /// [omit] + let internal buildNewArgs (param: NewOptions) = + [ + param.DryRun |> argOption "dry-run" + param.Force |> argOption "force" + argList2 "language" [param.Language.ToString()] + param.Name |> Option.toList |> argList2 "name" + param.NoUpdateCheck |> argOption "no-update-check" + param.Output |> Option.toList |> argList2 "output" + ] + |> List.concat + |> List.filter (not << String.IsNullOrEmpty) + + /// [omit] + let internal buildTemplateInstallArgs (param: TemplateInstallOptions) = + [ + argList2 "install" [param.Install] + param.NugetSource |> Option.toList |> argList2 "nuget-source" + ] + |> List.concat + |> List.filter (not << String.IsNullOrEmpty) + + /// [omit] + let internal buildTemplateUninstallArgs (param: TemplateUninstallOptions) = + [ + argList2 "uninstall" [param.Uninstall] + ] + |> List.concat + |> List.filter (not << String.IsNullOrEmpty) + + /// Execute dotnet new command + /// ## Parameters + /// + /// - 'templateName' - template short name to create from + /// - 'setParams' - set version command parameters + let newFromTemplate templateName setParams = + use __ = Trace.traceTask "DotNet:new" "dotnet new command" + let param = NewOptions.Create() |> setParams + let args = Args.toWindowsCommandLine(buildNewArgs param) + let result = exec (fun _ -> param.Common) $"new {templateName}" args + if not result.OK then failwithf $"dotnet new failed with code %i{result.ExitCode}" + __.MarkSuccess() + + /// Execute dotnet new --install command to install a new template + /// ## Parameters + /// + /// - 'templateName' - template short name to install + /// - 'setParams' - set version command parameters + let installTemplate templateName setParams = + use __ = Trace.traceTask "DotNet:new" "dotnet new --install command" + let param = TemplateInstallOptions.Create(templateName) |> setParams + let args = Args.toWindowsCommandLine(buildTemplateInstallArgs param) + let result = exec (fun _ -> param.Common) "new" args + if not result.OK then failwithf $"dotnet new --install failed with code %i{result.ExitCode}" + __.MarkSuccess() + + /// Execute dotnet new --uninstall command to uninstall a new template + /// ## Parameters + /// + /// - 'templateName' - template short name to uninstall + /// - 'setParams' - set version command parameters + let uninstallTemplate templateName = + use __ = Trace.traceTask "DotNet:new" "dotnet new --uninstall command" + let param = TemplateUninstallOptions.Create(templateName) + let args = Args.toWindowsCommandLine(buildTemplateUninstallArgs param) + let result = exec (fun _ -> param.Common) "new" args + + // we will check if the uninstall command has returned error and message is template is not found. + // if that is the case, then we will just redirect output as success and change process result to + // exit code of zero. + let templateIsNotFoundToUninstall = + result.Results + |> List.exists(fun (result:ConsoleMessage) -> result.Message.Contains $"The template package '{templateName}' is not found.") + + match templateIsNotFoundToUninstall with + | true -> ignore "" + | false -> failwithf $"dotnet new --uninstall failed with code %i{result.ExitCode}" + __.MarkSuccess() diff --git a/src/test/Fake.Core.UnitTests/Fake.DotNet.Cli.fs b/src/test/Fake.Core.UnitTests/Fake.DotNet.Cli.fs index aeeaeb6656a..32e28fadb91 100644 --- a/src/test/Fake.Core.UnitTests/Fake.DotNet.Cli.fs +++ b/src/test/Fake.Core.UnitTests/Fake.DotNet.Cli.fs @@ -82,4 +82,64 @@ let tests = let expected = "--configuration Release --manifest Path1 --manifest Path2" Expect.equal cli expected "Push args generated correctly." + + testCase "Test that the dotnet new command works as expected" <| fun _ -> + let param = + { DotNet.NewOptions.Create() with + DryRun = true + Force = true + Language = DotNet.NewLanguage.FSharp + Name = Some("my-awesome-project") + NoUpdateCheck = true + Output = Some("/path/to/code") } + let cli = + param + |> DotNet.buildNewArgs + |> Args.toWindowsCommandLine + + let expected = "--dry-run --force --language F# --name my-awesome-project --no-update-check --output /path/to/code" + + Expect.equal cli expected "New args generated correctly." + + testCase "Test that the dotnet new command works as expected with spaces in arguments" <| fun _ -> + let param = + { DotNet.NewOptions.Create() with + DryRun = true + Force = true + Language = DotNet.NewLanguage.FSharp + Name = Some("my awesome project") + NoUpdateCheck = true + Output = Some("/path to/code") } + let cli = + param + |> DotNet.buildNewArgs + |> Args.toWindowsCommandLine + + let expected = "--dry-run --force --language F# --name \"my awesome project\" --no-update-check --output \"/path to/code\"" + + Expect.equal cli expected "New args generated correctly." + + testCase "Test that the dotnet new --install command works as expected" <| fun _ -> + let param = + { DotNet.TemplateInstallOptions.Create("my-awesome-template") with + NugetSource = Some("C:\\path\\to\\tool") } + let cli = + param + |> DotNet.buildTemplateInstallArgs + |> Args.toWindowsCommandLine + + let expected = "--install my-awesome-template --nuget-source \"C:\\path\\to\\tool\"" + + Expect.equal cli expected "New --install args generated correctly." + + testCase "Test that the dotnet new --uninstall command works as expected" <| fun _ -> + let param = DotNet.TemplateUninstallOptions.Create("my-awesome-template") + let cli = + param + |> DotNet.buildTemplateUninstallArgs + |> Args.toWindowsCommandLine + + let expected = "--uninstall my-awesome-template" + + Expect.equal cli expected "New --uninstall args generated correctly." ] diff --git a/src/test/Fake.DotNet.Cli.IntegrationTests/TemplateTests.fs b/src/test/Fake.DotNet.Cli.IntegrationTests/TemplateTests.fs index a83cc9dba92..7e14373f9cb 100644 --- a/src/test/Fake.DotNet.Cli.IntegrationTests/TemplateTests.fs +++ b/src/test/Fake.DotNet.Cli.IntegrationTests/TemplateTests.fs @@ -2,7 +2,6 @@ open Expecto open System -open System.Linq open System.IO open Fake.Core @@ -13,7 +12,23 @@ let templateProj = "fake-template.fsproj" let templatePackageName = "fake-template" let templateName = "fake" -//TODO: add DotNetCli helpers for the `new` command +type BootstrapKind = + | Tool + | Local + | None + with override x.ToString () = match x with | Tool -> "tool" | Local -> "local" | None -> "none" + +type DslKind = + | Fake + | BuildTask + with override x.ToString () = match x with | Fake -> "fake" | BuildTask -> "buildtask" + +type DependenciesKind = + | File + | Inline + | None + with override x.ToString () = match x with | File -> "file" | Inline -> "inline" | None -> "none" + let dotnetSdk = lazy DotNet.install DotNet.Versions.FromGlobalJson @@ -22,109 +37,107 @@ let inline opts () = DotNet.Options.lift dotnetSdk.Value let inline dtntWorkDir wd = DotNet.Options.lift dotnetSdk.Value >> DotNet.Options.withWorkingDirectory wd - + let inline redirect () = DotNet.Options.lift (fun opts -> { opts with RedirectOutput = true }) -let uninstallTemplate () = - let result = DotNet.exec (opts() >> redirect()) "new" $"-u %s{templatePackageName}" - - // we will check if the install command has returned error and message is template is not found. - // if that is the case, then we will just redirect output as success and change process result to - // exit code of zero. - match result.Results.Any(fun (result:ConsoleMessage) -> result.Message.Equals $"The template package '{templatePackageName}' is not found.") with - | true -> ProcessResult.New 0 result.Results - | false -> result - -let installTemplateFrom pathToNupkg = - DotNet.exec (opts() >> redirect()) "new" (sprintf "-i %s" pathToNupkg) +let getDebuggingInfo() = + sprintf "%s\nDOTNET_ROOT: %s\nPATH: %s\n" (Environment.GetEnvironmentVariable("DOTNET_ROOT")) (Environment.GetEnvironmentVariable "PATH") -type BootstrapKind = -| Tool -| Local -| None -with override x.ToString () = match x with | Tool -> "tool" | Local -> "local" | None -> "none" - -type DslKind = -| Fake -| BuildTask -with override x.ToString () = match x with | Fake -> "fake" | BuildTask -> "buildtask" +let isProcessSucceeded message (r: ProcessResult) = + $"Message: {message}\n + Exit Code: {r.ExitCode}\n + Debugging Info: {getDebuggingInfo}\n + Result:\n stderr: {r.Result.Error}\n stdout: {r.Result.Output}" + |> Expect.isTrue (r.ExitCode = 0) -type DependenciesKind = -| File -| Inline -| None -with override x.ToString () = match x with | File -> "file" | Inline -> "inline" | None -> "none" - -let shouldSucceed message (r: ProcessResult) = - let errorStr = - r.Results - |> Seq.map (fun r -> sprintf "%s: %s" (if r.IsError then "stderr" else "stdout") r.Message) - |> fun s -> String.Join("\n", s) - Expect.isTrue - r.OK - (sprintf - "%s. Exit code '%d'.\nDOTNET_ROOT: %s\nPATH: %s\n Results:\n%s\n" - message r.ExitCode (Environment.GetEnvironmentVariable("DOTNET_ROOT")) - (Environment.GetEnvironmentVariable "PATH") errorStr) - -let timeout = (System.TimeSpan.FromMinutes 10.) +let timeout = (TimeSpan.FromMinutes 10.) let runTemplate rootDir kind dependencies dsl = Directory.ensure rootDir try - DotNet.exec (dtntWorkDir rootDir >> redirect()) "new" (sprintf "%s --allow-scripts yes --bootstrap %s --dependencies %s --dsl %s" templateName (string kind) (string dependencies) (string dsl)) - |> shouldSucceed "should have run the template successfully" + let result = + DotNet.exec (dtntWorkDir rootDir >> redirect()) "new" $"{templateName} --allow-scripts yes --bootstrap {string kind} --dependencies {string dependencies} --dsl {string dsl}" + + let errors = + result.Results + |> List.filter(fun res -> res.IsError) + |> List.map(fun res -> res.Message) + + let messages = + result.Results + |> List.filter(fun res -> not res.IsError) + |> List.map(fun res -> res.Message) + + let processResult: ProcessResult = { + ExitCode = result.ExitCode + Result = {Output = String.Join ("\n", messages); Error = String.Join ("\n", errors)} + } + + isProcessSucceeded "should have run the template successfully" processResult with e -> if e.Message.Contains "Command succeeded" && e.Message.Contains "was created successfully" then - printfn "Ignoring exit-code while template creation: %O" e + printfn $"Ignoring exit-code while template creation: {e}" else reraise() - -let invokeScript dir scriptName args = +let invokeScript dir scriptName (args: string) = let fullScriptPath = Path.Combine(dir, scriptName) - - Process.execWithResult - (fun x -> - x.WithWorkingDirectory(dir) - .WithFileName(fullScriptPath) - .WithArguments args) timeout + CreateProcess.fromRawCommandLine fullScriptPath args + |> CreateProcess.withTimeout timeout + |> CreateProcess.withWorkingDirectory dir + |> CreateProcess.redirectOutput + |> Proc.run let fileContainsText dir fileName text = let filePath = Path.Combine(dir, fileName) let content = File.ReadAllText(filePath) content.Contains(text: string) -let expectMissingTarget targetName (r: ProcessResult) = - let contains = r.Errors |> Seq.exists (fun err -> err.Contains (sprintf "Target \"%s\" is not defined" targetName)) - Expect.isTrue contains (sprintf "Expected the message 'Target %%s is not defined' but got: %s" (String.Join("\n", r.Errors))) +let expectMissingTarget targetName (r: ProcessResult) = + let contains = r.Result.Error.Contains $"Target \"{targetName}\" is not defined" + Expect.isTrue contains $"Expected the message 'Target {targetName} is not defined' but got: {r.Result.Error}" let tempDir() = Path.Combine("../../../test/fake-template", Path.GetRandomFileName()) let fileExists dir fileName = File.Exists(Path.Combine(dir, fileName)) +let setupTemplate() = + Process.setEnableProcessTracing true + + try + DotNet.uninstallTemplate templatePackageName + with exn -> + $"should clear out preexisting templates\nDebugging Info: {getDebuggingInfo}" + |> Expect.isTrue false + + printfn $"%s{Environment.CurrentDirectory}" + + DotNet.setupEnv dotnetSdk.Value + + let templateNupkg = + GlobbingPattern.create "../../../release/dotnetcore/fake-template.*.nupkg" + |> GlobbingPattern.setBaseDir __SOURCE_DIRECTORY__ + |> Seq.toList + |> List.rev + |> List.tryHead + + let fakeTemplateName = + match templateNupkg with + | Some t -> t + | Option.None -> templatePackageName + try + DotNet.installTemplate fakeTemplateName id + with exn -> + $"should install new FAKE template\nDebugging Info: {getDebuggingInfo}" + |> Expect.isTrue false + [] let tests = // we need to (uninstall) the template, install the packed version, and then execute that template testList "Fake.DotNet.Cli.IntegrationTests.Template tests" [ testList "can install and run the template" [ - Process.setEnableProcessTracing true - uninstallTemplate () |> shouldSucceed "should clear out preexisting templates" - printfn "%s" Environment.CurrentDirectory - - DotNet.setupEnv dotnetSdk.Value - let templateNupkg = - GlobbingPattern.create "../../../release/dotnetcore/fake-template.*.nupkg" - |> GlobbingPattern.setBaseDir __SOURCE_DIRECTORY__ - |> Seq.toList - |> List.rev - |> List.tryHead - let installArgument = - match templateNupkg with - | Some t -> t - | Option.None -> "fake-template" - installTemplateFrom installArgument |> shouldSucceed "should install new FAKE template" + setupTemplate() let scriptFile = if Environment.isUnix @@ -139,7 +152,7 @@ let tests = runTemplate tempDir Tool File Fake Expect.isFalse (Directory.Exists (Path.Combine(tempDir, ".fake"))) "After creating the template the '.fake' directory should not exist!" let result = invokeScript tempDir scriptFile "build -t Nonexistent" - Expect.isFalse result.OK "the script should have failed" + Expect.isFalse (result.ExitCode = 0) "the script should have failed" expectMissingTarget "Nonexistent" result } @@ -166,7 +179,8 @@ let tests = runTemplate tempDir Tool File BuildTask Expect.isFalse (Directory.Exists (Path.Combine(tempDir, ".fake"))) "After creating the template the '.fake' directory should not exist!" - invokeScript tempDir scriptFile "build -t All" |> shouldSucceed "should build successfully" + invokeScript tempDir scriptFile "build -t All" + |> isProcessSucceeded "should build successfully" } yield test "can install a buildtask-dsl inline-dependencies template" { @@ -182,15 +196,14 @@ let tests = runTemplate tempDir Tool Inline BuildTask Expect.isFalse (Directory.Exists (Path.Combine(tempDir, ".fake"))) "After creating the template the '.fake' directory should not exist!" - invokeScript tempDir scriptFile "build -t All" |> shouldSucceed "should build successfully" + invokeScript tempDir scriptFile "build -t All" |> isProcessSucceeded "should build successfully" } - // Enable after https://github.com/fsharp/FAKE/pull/2403 - //yield test "can build with the local-style template" { - // let tempDir = tempDir() - // runTemplate tempDir Local Inline BuildTask - // invokeScript tempDir scriptFile "build -t All" |> shouldSucceed "should build successfully" - //} + yield test "can build with the local-style template" { + let tempDir = tempDir() + runTemplate tempDir Local Inline BuildTask + invokeScript tempDir scriptFile "build -t All" |> isProcessSucceeded "should build successfully" + } /// ignored because the .net tool install to a subdirectory is broken: https://github.com/fsharp/FAKE/pull/1989#issuecomment-396057330 yield ptest "can install a tool-style template" { @@ -198,7 +211,7 @@ let tests = runTemplate tempDir Tool File Fake Expect.isFalse (Directory.Exists (Path.Combine(tempDir, ".fake"))) "After creating the template the '.fake' directory should not exist!" - invokeScript tempDir scriptFile "--help" |> shouldSucceed "should invoke help" + invokeScript tempDir scriptFile "--help" |> isProcessSucceeded "should invoke help" } ] ]