diff --git a/CliWrap.FSharp.sln b/CliWrap.FSharp.sln index 882bfbf..7b2893e 100644 --- a/CliWrap.FSharp.sln +++ b/CliWrap.FSharp.sln @@ -1,4 +1,4 @@ - + Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 17 VisualStudioVersion = 17.0.31903.59 @@ -20,6 +20,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "build", "build", "{BE2AB152 global.json = global.json .config\dotnet-tools.json = .config\dotnet-tools.json Directory.Build.props = Directory.Build.props + scripts\update-vendored-code.sh = scripts\update-vendored-code.sh + patches\0001-CliWrap.Tests.Dummy.csproj.patch = patches\0001-CliWrap.Tests.Dummy.csproj.patch EndProjectSection EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "github", "github", "{FE346152-5716-4224-9B25-C6B3815C70CA}" @@ -40,9 +42,9 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "config", "config", "{BC5059 .gitmodules = .gitmodules EndProjectSection EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "vendor", "vendor", "{711E438A-D184-4513-921C-C4D7AE85E2B9}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CliWrap.Tests.Dummy", "src\CliWrap.Tests.Dummy\CliWrap.Tests.Dummy.csproj", "{26F29434-8760-4387-BF01-4FB1FA65426E}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CliWrap.Tests.Dummy", "vendor\CliWrap\CliWrap.Tests.Dummy\CliWrap.Tests.Dummy.csproj", "{C06FC79C-B308-4078-8C36-AB2F42C583E0}" +Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "CliWrap.FSharp.Tests.Dummy", "src\CliWrap.FSharp.Tests.Dummy\CliWrap.FSharp.Tests.Dummy.fsproj", "{47479E30-792D-4B63-A1A9-D5981968D21E}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -61,14 +63,19 @@ Global {12BFD7AA-776D-4B58-8AC3-241AF39EED84}.Debug|Any CPU.Build.0 = Debug|Any CPU {12BFD7AA-776D-4B58-8AC3-241AF39EED84}.Release|Any CPU.ActiveCfg = Release|Any CPU {12BFD7AA-776D-4B58-8AC3-241AF39EED84}.Release|Any CPU.Build.0 = Release|Any CPU - {C06FC79C-B308-4078-8C36-AB2F42C583E0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {C06FC79C-B308-4078-8C36-AB2F42C583E0}.Debug|Any CPU.Build.0 = Debug|Any CPU - {C06FC79C-B308-4078-8C36-AB2F42C583E0}.Release|Any CPU.ActiveCfg = Release|Any CPU - {C06FC79C-B308-4078-8C36-AB2F42C583E0}.Release|Any CPU.Build.0 = Release|Any CPU + {26F29434-8760-4387-BF01-4FB1FA65426E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {26F29434-8760-4387-BF01-4FB1FA65426E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {26F29434-8760-4387-BF01-4FB1FA65426E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {26F29434-8760-4387-BF01-4FB1FA65426E}.Release|Any CPU.Build.0 = Release|Any CPU + {47479E30-792D-4B63-A1A9-D5981968D21E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {47479E30-792D-4B63-A1A9-D5981968D21E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {47479E30-792D-4B63-A1A9-D5981968D21E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {47479E30-792D-4B63-A1A9-D5981968D21E}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(NestedProjects) = preSolution {2E2FBB43-30C0-45A1-8DAC-D7A48411EC7A} = {DE85DD40-1128-4648-A2EB-9A1FE73C28A4} {12BFD7AA-776D-4B58-8AC3-241AF39EED84} = {DE85DD40-1128-4648-A2EB-9A1FE73C28A4} - {C06FC79C-B308-4078-8C36-AB2F42C583E0} = {711E438A-D184-4513-921C-C4D7AE85E2B9} + {26F29434-8760-4387-BF01-4FB1FA65426E} = {DE85DD40-1128-4648-A2EB-9A1FE73C28A4} + {47479E30-792D-4B63-A1A9-D5981968D21E} = {DE85DD40-1128-4648-A2EB-9A1FE73C28A4} EndGlobalSection EndGlobal diff --git a/patches/0001-CliWrap.Tests.Dummy.csproj.patch b/patches/0001-CliWrap.Tests.Dummy.csproj.patch new file mode 100644 index 0000000..c488e50 --- /dev/null +++ b/patches/0001-CliWrap.Tests.Dummy.csproj.patch @@ -0,0 +1,13 @@ +5,6c5,6 +< net8.0 +< $(TargetFrameworks);net48 +--- +> net8.0 +> false +11d10 +< +16c15 +< +\ No newline at end of file +--- +> diff --git a/scripts/update-vendored-code.sh b/scripts/update-vendored-code.sh new file mode 100755 index 0000000..43ee47e --- /dev/null +++ b/scripts/update-vendored-code.sh @@ -0,0 +1,15 @@ +#!/bin/bash +set -eum + +root="$(git rev-parse --show-toplevel)" +patch="$root/patches/0001-CliWrap.Tests.Dummy.csproj.patch" +vendorDir="$root/vendor/CliWrap" +targetDir="$root/src/CliWrap.Tests.Dummy" + +git submodule foreach --recursive git clean -xfd +rm -rf "$targetDir" || echo 'Target is clean' + +[ '--clean' == "${1:-}" ] && exit 0 + +cp -r "$vendorDir/CliWrap.Tests.Dummy" "$targetDir" +patch "$targetDir/CliWrap.Tests.Dummy.csproj" <"$patch" diff --git a/src/CliWrap.FSharp.Tests.Dummy/CliWrap.FSharp.Tests.Dummy.fsproj b/src/CliWrap.FSharp.Tests.Dummy/CliWrap.FSharp.Tests.Dummy.fsproj new file mode 100644 index 0000000..fb60bdd --- /dev/null +++ b/src/CliWrap.FSharp.Tests.Dummy/CliWrap.FSharp.Tests.Dummy.fsproj @@ -0,0 +1,17 @@ + + + + Exe + net8.0 + UnMango.CliWrap.FSharp.Tests.Dummy + + + + + + + + + + + diff --git a/src/CliWrap.FSharp.Tests.Dummy/Program.fs b/src/CliWrap.FSharp.Tests.Dummy/Program.fs new file mode 100644 index 0000000..32c4fa0 --- /dev/null +++ b/src/CliWrap.FSharp.Tests.Dummy/Program.fs @@ -0,0 +1,81 @@ +namespace UnMango.CliWrap.FSharp.Tests.Dummy + +open System +open System.Buffers +open System.CommandLine +open System.CommandLine.Invocation +open System.IO +open System.Reflection +open System.Runtime.InteropServices +open System.Threading.Tasks + +module Opt = + let add o (r: Command) = + r.AddOption(o) + r + +module Cmd = + let add c (r: Command) = + r.AddCommand(c) + r + + let handler (f: InvocationContext -> Task) (c: Command) = + c.SetHandler(f) + c + +module Commands = + let streams = + function + | "stdout" -> [ Console.OpenStandardOutput() ] + | "stderr" -> [ Console.OpenStandardError() ] + | "all" -> [ Console.OpenStandardOutput(); Console.OpenStandardError() ] + | _ -> failwith "unsupported target" + + let rec generateBytes (rand: Random) (buf: IMemoryOwner) (streams: Stream list) len = + function + | total when total >= len -> () + | total -> + rand.NextBytes(buf.Memory.Span) + let wanted = Math.Min(int64 buf.Memory.Length, len - total) + + for stream in streams do + stream.Write(buf.Memory.Slice(0, int wanted).Span) + + generateBytes rand buf streams len (total + wanted) + + let generate = + let targetOpt = Option("--target", (fun () -> "stdout")) + let lengthOpt = Option("--length", (fun () -> 100_000L)) + let bufferOpt = Option("--buffer", (fun () -> 1024)) + let rand = Random(1234567) + + Command("generate") + |> Cmd.add ( + Command("binary") + |> Opt.add targetOpt + |> Opt.add lengthOpt + |> Opt.add bufferOpt + |> Cmd.handler (fun c -> task { + let streams = c.ParseResult.GetValueForOption(targetOpt) |> streams + let len = c.ParseResult.GetValueForOption(lengthOpt) + let size = c.ParseResult.GetValueForOption(bufferOpt) + use buf = MemoryPool.Shared.Rent(size) + generateBytes rand buf streams len 0L + }) + ) + +type Program = + static member FilePath = + Path.ChangeExtension( + Assembly.GetExecutingAssembly().Location, + if RuntimeInformation.IsOSPlatform(OSPlatform.Windows) then + "exe" + else + null + ) + +module Program = + let command = RootCommand("Dummy program for testing") |> Cmd.add Commands.generate + + [] + let main args = command.Invoke args diff --git a/src/CliWrap.FSharp.Tests.Dummy/packages.lock.json b/src/CliWrap.FSharp.Tests.Dummy/packages.lock.json new file mode 100644 index 0000000..f9c2be0 --- /dev/null +++ b/src/CliWrap.FSharp.Tests.Dummy/packages.lock.json @@ -0,0 +1,19 @@ +{ + "version": 1, + "dependencies": { + "net8.0": { + "FSharp.Core": { + "type": "Direct", + "requested": "[8.0.300, )", + "resolved": "8.0.300", + "contentHash": "Jv44fV7TNglyMku89lQcA4Q6mFKLyHb2bs1Yb72nvSVc+cHplEnoZ4XQUaaTLJGUTx/iMqcrkYGtaLzkkIhpaA==" + }, + "System.CommandLine": { + "type": "Direct", + "requested": "[2.0.0-beta4.22272.1, )", + "resolved": "2.0.0-beta4.22272.1", + "contentHash": "1uqED/q2H0kKoLJ4+hI2iPSBSEdTuhfCYADeJrAqERmiGQ2NNacYKRNEQ+gFbU4glgVyK8rxI+ZOe1onEtr/Pg==" + } + } + } +} \ No newline at end of file diff --git a/src/CliWrap.FSharp.Tests/CliTests.fs b/src/CliWrap.FSharp.Tests/CliTests.fs index 80068d8..5591d20 100644 --- a/src/CliWrap.FSharp.Tests/CliTests.fs +++ b/src/CliWrap.FSharp.Tests/CliTests.fs @@ -5,7 +5,6 @@ open System.Linq open System.Text open System.Threading open CliWrap -open CliWrap.Tests open Xunit open FsCheck open FsCheck.Xunit @@ -40,7 +39,7 @@ let ``Should create command the long way`` [] let ``Should configure a single argument`` (arg: NonNull) = - let cmd = Command(Dummy.Program.FilePath) + let cmd = Command(Tests.Dummy.Program.FilePath) let expected = cmd.WithArguments(arg.Get) let actual = cmd |> Cli.arg arg.Get @@ -49,7 +48,7 @@ let ``Should configure a single argument`` (arg: NonNull) = [] let ``Should configure arguments`` (arg: NonNull) = - let cmd = Command(Dummy.Program.FilePath) + let cmd = Command(Tests.Dummy.Program.FilePath) let expected = cmd.WithArguments([ arg.Get ]) let actual = cmd |> Cli.args [ arg.Get ] @@ -58,7 +57,7 @@ let ``Should configure arguments`` (arg: NonNull) = [] let ``Should configure arguments with escape`` (arg: NonNull) escape = - let cmd = Command(Dummy.Program.FilePath) + let cmd = Command(Tests.Dummy.Program.FilePath) let expected = cmd.WithArguments([ arg.Get ], escape) let actual = cmd |> Cli.argse [ arg.Get ] escape @@ -67,7 +66,7 @@ let ``Should configure arguments with escape`` (arg: NonNull) escape = [] let ``Should configure arguments with builder`` (arg: NonNull) = - let cmd = Command(Dummy.Program.FilePath) + let cmd = Command(Tests.Dummy.Program.FilePath) let expected = cmd.WithArguments(fun b -> b.Add(arg.Get) |> ignore) let actual = cmd |> Cli.argsf _.Add(arg.Get) @@ -76,7 +75,7 @@ let ``Should configure arguments with builder`` (arg: NonNull) = [] let ``Should configure credentials`` (arg: NonNull) = - let cmd = Command(Dummy.Program.FilePath) + let cmd = Command(Tests.Dummy.Program.FilePath) let expected = cmd.WithCredentials(Credentials(arg.Get)) let actual = cmd |> Cli.creds (Credentials(arg.Get)) @@ -85,7 +84,7 @@ let ``Should configure credentials`` (arg: NonNull) = [] let ``Should configure credentials with builder`` (arg: NonNull) = - let cmd = Command(Dummy.Program.FilePath) + let cmd = Command(Tests.Dummy.Program.FilePath) let expected = cmd.WithCredentials(fun b -> b.SetUserName(arg.Get) |> ignore) let actual = cmd |> Cli.credsf _.SetUserName(arg.Get) @@ -94,7 +93,7 @@ let ``Should configure credentials with builder`` (arg: NonNull) = [] let ``Should configure environment variables`` (key: NonNull) (value: NonNull) = - let cmd = Command(Dummy.Program.FilePath) + let cmd = Command(Tests.Dummy.Program.FilePath) let expected = cmd.WithEnvironmentVariables((dict [ key.Get, value.Get ]).AsReadOnly()) @@ -104,7 +103,7 @@ let ``Should configure environment variables`` (key: NonNull) (value: No [] let ``Should configure environment variables with builder`` (key: NonNull) (value: NonNull) = - let cmd = Command(Dummy.Program.FilePath) + let cmd = Command(Tests.Dummy.Program.FilePath) let expected = cmd.WithEnvironmentVariables(fun b -> b.Set(key.Get, value.Get) |> ignore) @@ -114,7 +113,7 @@ let ``Should configure environment variables with builder`` (key: NonNull] let ``Should execute asynchronously`` () = task { - let cmd = Command(Dummy.Program.FilePath) + let cmd = Command(Tests.Dummy.Program.FilePath) let! expected = cmd.ExecuteAsync() let! actual = cmd |> Cli.exec @@ -125,7 +124,7 @@ let ``Should execute asynchronously`` () = task { [] let ``Should execute asynchronously with cancellation`` () = task { use cts = new CancellationTokenSource() - let cmd = Command(Dummy.Program.FilePath) + let cmd = Command(Tests.Dummy.Program.FilePath) let! expected = cmd.ExecuteAsync(cts.Token) let! actual = cmd |> Cli.Task.exec cts.Token @@ -137,7 +136,7 @@ let ``Should execute asynchronously with cancellation`` () = task { let ``Should execute asynchronously with forceful and graceful tokens`` () = task { use forceful = new CancellationTokenSource() use graceful = new CancellationTokenSource() - let cmd = Command(Dummy.Program.FilePath) + let cmd = Command(Tests.Dummy.Program.FilePath) let! expected = cmd.ExecuteAsync(forceful.Token, graceful.Token) let! actual = cmd |> Cli.Task.execf forceful.Token graceful.Token @@ -148,7 +147,7 @@ let ``Should execute asynchronously with forceful and graceful tokens`` () = tas [] let ``Should configure stdin`` (value: NonNull) = let input = PipeSource.FromString value.Get - let cmd = Command(Dummy.Program.FilePath) + let cmd = Command(Tests.Dummy.Program.FilePath) let expected = cmd.WithStandardInputPipe(input) let actual = cmd |> Cli.stdin input @@ -158,7 +157,7 @@ let ``Should configure stdin`` (value: NonNull) = [] let ``Should configure stdout`` () = let pipe = PipeTarget.ToStringBuilder(StringBuilder()) - let cmd = Command(Dummy.Program.FilePath) + let cmd = Command(Tests.Dummy.Program.FilePath) let expected = cmd.WithStandardOutputPipe(pipe) let actual = cmd |> Cli.stdout pipe @@ -168,7 +167,7 @@ let ``Should configure stdout`` () = [] let ``Should configure stderr`` () = let pipe = PipeTarget.ToStringBuilder(StringBuilder()) - let cmd = Command(Dummy.Program.FilePath) + let cmd = Command(Tests.Dummy.Program.FilePath) let expected = cmd.WithStandardErrorPipe(pipe) let actual = cmd |> Cli.stderr pipe @@ -177,7 +176,7 @@ let ``Should configure stderr`` () = [] let ``Should configure target file`` (file: NonNull) = - let cmd = Command(Dummy.Program.FilePath) + let cmd = Command(Tests.Dummy.Program.FilePath) let expected = cmd.WithTargetFile(file.Get) let actual = cmd |> Cli.target file.Get @@ -186,7 +185,7 @@ let ``Should configure target file`` (file: NonNull) = [] let ``Should configure validation`` () = - let cmd = Command(Dummy.Program.FilePath) + let cmd = Command(Tests.Dummy.Program.FilePath) let expected = cmd.WithValidation(CommandResultValidation.None) let actual = cmd |> Cli.validation CommandResultValidation.None @@ -195,7 +194,7 @@ let ``Should configure validation`` () = [] let ``Should configure working directory`` (dir: NonNull) = - let cmd = Command(Dummy.Program.FilePath) + let cmd = Command(Tests.Dummy.Program.FilePath) let expected = cmd.WithWorkingDirectory(dir.Get) let actual = cmd |> Cli.workDir dir.Get @@ -204,7 +203,7 @@ let ``Should configure working directory`` (dir: NonNull) = [] let ``Should convert to string`` (arg: NonNull) = - let cmd = Command(Dummy.Program.FilePath).WithArguments([ arg.Get ]) + let cmd = Command(Tests.Dummy.Program.FilePath).WithArguments([ arg.Get ]) let expected = cmd.ToString() let actual = cmd |> Cli.toString diff --git a/src/CliWrap.FSharp.Tests/CliWrap.FSharp.Tests.fsproj b/src/CliWrap.FSharp.Tests/CliWrap.FSharp.Tests.fsproj index 60bf36e..8bb7c93 100644 --- a/src/CliWrap.FSharp.Tests/CliWrap.FSharp.Tests.fsproj +++ b/src/CliWrap.FSharp.Tests/CliWrap.FSharp.Tests.fsproj @@ -25,7 +25,7 @@ - + diff --git a/src/CliWrap.FSharp.Tests/CommandBuilderTests.fs b/src/CliWrap.FSharp.Tests/CommandBuilderTests.fs index 21aa4af..7e9289f 100644 --- a/src/CliWrap.FSharp.Tests/CommandBuilderTests.fs +++ b/src/CliWrap.FSharp.Tests/CommandBuilderTests.fs @@ -7,7 +7,6 @@ open System.Text open System.Threading open CliWrap open CliWrap.Buffered -open CliWrap.Tests open Xunit open FsCheck open FsCheck.Xunit @@ -22,22 +21,22 @@ let ``Should configure target file path`` target = [] let ``Should configure args`` (a: NonNull list) = let input = a |> List.map _.Get - let expected = Command(Dummy.Program.FilePath).WithArguments(input) - let actual = command Dummy.Program.FilePath { args input } + let expected = Command(Tests.Dummy.Program.FilePath).WithArguments(input) + let actual = command Tests.Dummy.Program.FilePath { args input } actual.Arguments = expected.Arguments [] let ``Should configure string args`` (a: NonNull) = - let expected = Command(Dummy.Program.FilePath).WithArguments(a.Get) - let actual = command Dummy.Program.FilePath { args a.Get } + let expected = Command(Tests.Dummy.Program.FilePath).WithArguments(a.Get) + let actual = command Tests.Dummy.Program.FilePath { args a.Get } actual.Arguments = expected.Arguments [] let ``Should execute CommandTask asynchronously`` () = task { - let! expected = Command(Dummy.Program.FilePath).ExecuteAsync() + let! expected = Command(Tests.Dummy.Program.FilePath).ExecuteAsync() let! actual = - command Dummy.Program.FilePath { + command Tests.Dummy.Program.FilePath { exec async } @@ -48,38 +47,38 @@ let ``Should execute CommandTask asynchronously`` () = task { [] let ``Should execute asynchronously`` () = task { - let! expected = Command(Dummy.Program.FilePath).ExecuteAsync() - let! actual = command Dummy.Program.FilePath { async } |> Async.StartAsTask + let! expected = Command(Tests.Dummy.Program.FilePath).ExecuteAsync() + let! actual = command Tests.Dummy.Program.FilePath { async } |> Async.StartAsTask Assert.Equal(expected.ExitCode, actual.ExitCode) } [] let ``Should execute asynchronously with cancellation`` () = task { use cts = new CancellationTokenSource() - let! expected = Command(Dummy.Program.FilePath).ExecuteAsync(cts.Token) - let! actual = command Dummy.Program.FilePath { async cts.Token } |> Async.StartAsTask + let! expected = Command(Tests.Dummy.Program.FilePath).ExecuteAsync(cts.Token) + let! actual = command Tests.Dummy.Program.FilePath { async cts.Token } |> Async.StartAsTask Assert.Equal(expected.ExitCode, actual.ExitCode) } [] let ``Should execute asynchronously as task`` () = task { - let! expected = Command(Dummy.Program.FilePath).ExecuteAsync() - let! actual = command Dummy.Program.FilePath { exec } + let! expected = Command(Tests.Dummy.Program.FilePath).ExecuteAsync() + let! actual = command Tests.Dummy.Program.FilePath { exec } Assert.Equal(expected.ExitCode, actual.ExitCode) } [] let ``Should execute buffered`` () = task { - let! expected = Command(Dummy.Program.FilePath).ExecuteBufferedAsync() - let! actual = command Dummy.Program.FilePath { buffered } + let! expected = Command(Tests.Dummy.Program.FilePath).ExecuteBufferedAsync() + let! actual = command Tests.Dummy.Program.FilePath { buffered } Assert.Equal(expected.ExitCode, actual.ExitCode) } [] let ``Should execute buffered with encoding`` () = task { let encoding = Encoding.UTF8 - let! expected = Command(Dummy.Program.FilePath).ExecuteBufferedAsync(encoding) - let! actual = command Dummy.Program.FilePath { buffered encoding } + let! expected = Command(Tests.Dummy.Program.FilePath).ExecuteBufferedAsync(encoding) + let! actual = command Tests.Dummy.Program.FilePath { buffered encoding } Assert.Equal(expected.ExitCode, actual.ExitCode) } @@ -87,26 +86,26 @@ let ``Should execute buffered with encoding`` () = task { let ``Should execute buffered with encoding and cancellation`` () = task { let encoding = Encoding.UTF8 use cts = new CancellationTokenSource() - let! expected = Command(Dummy.Program.FilePath).ExecuteBufferedAsync(encoding, cts.Token) - let! actual = command Dummy.Program.FilePath { buffered encoding cts.Token } + let! expected = Command(Tests.Dummy.Program.FilePath).ExecuteBufferedAsync(encoding, cts.Token) + let! actual = command Tests.Dummy.Program.FilePath { buffered encoding cts.Token } Assert.Equal(expected.ExitCode, actual.ExitCode) } [] let ``Should configure environment variables`` var = let expected = - Command(Dummy.Program.FilePath) + Command(Tests.Dummy.Program.FilePath) .WithEnvironmentVariables((dict [ var ]).AsReadOnly()) - let actual = command Dummy.Program.FilePath { env [ var ] } + let actual = command Tests.Dummy.Program.FilePath { env [ var ] } actual.EnvironmentVariables.SequenceEqual(expected.EnvironmentVariables) [] let ``Should configure stdin`` () = task { let input = PipeSource.FromString "testing" - let expected = Command(Dummy.Program.FilePath).WithStandardInputPipe(input) + let expected = Command(Tests.Dummy.Program.FilePath).WithStandardInputPipe(input) - let actual = command Dummy.Program.FilePath { stdin input } + let actual = command Tests.Dummy.Program.FilePath { stdin input } let a, b = new MemoryStream(), new MemoryStream() do! expected.StandardInputPipe.CopyToAsync(a) @@ -120,13 +119,13 @@ let ``Should configure stdout`` () = task { let a, b = StringBuilder(), StringBuilder() let! _ = - Command(Dummy.Program.FilePath) - .WithArguments([ "generate binary"; "--target"; "all" ]) + Command(Tests.Dummy.Program.FilePath) + .WithArguments([ "generate"; "binary"; "--target"; "all" ]) .WithStandardOutputPipe(PipeTarget.ToStringBuilder(a)) .ExecuteAsync() - let! _ = command Dummy.Program.FilePath { - args [ "generate binary"; "--target"; "all" ] + let! _ = command Tests.Dummy.Program.FilePath { + args [ "generate"; "binary"; "--target"; "all" ] stdout (PipeTarget.ToStringBuilder(b)) exec } @@ -139,13 +138,13 @@ let ``Should configure stderr`` () = task { let a, b = StringBuilder(), StringBuilder() let! _ = - Command(Dummy.Program.FilePath) - .WithArguments([ "generate binary"; "--target"; "all" ]) + Command(Tests.Dummy.Program.FilePath) + .WithArguments([ "generate"; "binary"; "--target"; "all" ]) .WithStandardErrorPipe(PipeTarget.ToStringBuilder(a)) .ExecuteAsync() - let! _ = command Dummy.Program.FilePath { - args [ "generate binary"; "--target"; "all" ] + let! _ = command Tests.Dummy.Program.FilePath { + args [ "generate"; "binary"; "--target"; "all" ] validation CommandResultValidation.None stderr (PipeTarget.ToStringBuilder(b)) exec @@ -156,6 +155,6 @@ let ``Should configure stderr`` () = task { [] let ``Should configure working directory`` directory = - let expected = Command(Dummy.Program.FilePath).WithWorkingDirectory(directory) - let actual = command Dummy.Program.FilePath { workingDirectory directory } + let expected = Command(Tests.Dummy.Program.FilePath).WithWorkingDirectory(directory) + let actual = command Tests.Dummy.Program.FilePath { workingDirectory directory } actual.WorkingDirPath = expected.WorkingDirPath diff --git a/src/CliWrap.FSharp.Tests/packages.lock.json b/src/CliWrap.FSharp.Tests/packages.lock.json index f6e1762..3e80df5 100644 --- a/src/CliWrap.FSharp.Tests/packages.lock.json +++ b/src/CliWrap.FSharp.Tests/packages.lock.json @@ -29,9 +29,9 @@ }, "FSharp.Core": { "type": "Direct", - "requested": "[8.0.200, )", - "resolved": "8.0.200", - "contentHash": "qnxoF3Fu0HzfOeYdrwmQOsLP1v+OtOMSIYkNVUwf6nGqWzL03Hh4r6VFCvCb54jlsgtt3WADVYkKkrgdeY5kiQ==" + "requested": "[8.0.300, )", + "resolved": "8.0.300", + "contentHash": "Jv44fV7TNglyMku89lQcA4Q6mFKLyHb2bs1Yb72nvSVc+cHplEnoZ4XQUaaTLJGUTx/iMqcrkYGtaLzkkIhpaA==" }, "Microsoft.NET.Test.Sdk": { "type": "Direct", @@ -60,11 +60,6 @@ "resolved": "2.5.7", "contentHash": "31Rl7dBJriX0DNwZfDp8gqFOPsiM0c9kqpcH/HvNi9vDp+K7Ydf42H7mVIvYT918Ywzn1ymLg1c4DDC6iU754w==" }, - "CliFx": { - "type": "Transitive", - "resolved": "2.3.5", - "contentHash": "WaZDt7FC1ViEzS5m7w7lOWT/aluZ28fFkPcRdkQ+7DowqjTmr5YZswNIDqSuBDaeGXPe8VzTZKMltKrXhIOt9Q==" - }, "CliWrap": { "type": "Transitive", "resolved": "3.6.6", @@ -97,10 +92,10 @@ "resolved": "13.0.1", "contentHash": "ppPFpBcvxdsfUonNcvITKqLl3bqxWbDCZIzDWHzjpdAHRFfZe0Dw9HmA0+za13IdyrgJwpkDTDA9fHaxOrt20A==" }, - "System.Memory": { + "System.CommandLine": { "type": "Transitive", - "resolved": "4.5.5", - "contentHash": "XIWiDvKPXaTveaB7HVganDlOCRoj03l+jrwNvcge/t8vhGYKvqV+dMv6G4SAX2NoNmN0wZfVPTAlFwZcZvVOUw==" + "resolved": "2.0.0-beta4.22272.1", + "contentHash": "1uqED/q2H0kKoLJ4+hI2iPSBSEdTuhfCYADeJrAqERmiGQ2NNacYKRNEQ+gFbU4glgVyK8rxI+ZOe1onEtr/Pg==" }, "System.Reflection.Metadata": { "type": "Transitive", @@ -147,18 +142,18 @@ "xunit.extensibility.core": "[2.7.0]" } }, - "cliwrap.tests.dummy": { + "cliwrap.fsharp.tests.dummy": { "type": "Project", "dependencies": { - "CliFx": "[2.3.5, )", - "System.Memory": "[4.5.5, )" + "FSharp.Core": "[8.0.300, )", + "System.CommandLine": "[2.0.0-beta4.22272.1, )" } }, "UnMango.CliWrap.FSharp": { "type": "Project", "dependencies": { "CliWrap": "[3.6.6, )", - "FSharp.Core": "[8.0.200, )" + "FSharp.Core": "[8.0.300, )" } } } diff --git a/src/CliWrap.FSharp/packages.lock.json b/src/CliWrap.FSharp/packages.lock.json index 3d666e3..78ad91a 100644 --- a/src/CliWrap.FSharp/packages.lock.json +++ b/src/CliWrap.FSharp/packages.lock.json @@ -10,9 +10,9 @@ }, "FSharp.Core": { "type": "Direct", - "requested": "[8.0.200, )", - "resolved": "8.0.200", - "contentHash": "qnxoF3Fu0HzfOeYdrwmQOsLP1v+OtOMSIYkNVUwf6nGqWzL03Hh4r6VFCvCb54jlsgtt3WADVYkKkrgdeY5kiQ==" + "requested": "[8.0.300, )", + "resolved": "8.0.300", + "contentHash": "Jv44fV7TNglyMku89lQcA4Q6mFKLyHb2bs1Yb72nvSVc+cHplEnoZ4XQUaaTLJGUTx/iMqcrkYGtaLzkkIhpaA==" }, "Microsoft.SourceLink.GitHub": { "type": "Direct", diff --git a/src/CliWrap.Tests.Dummy/CliWrap.Tests.Dummy.csproj b/src/CliWrap.Tests.Dummy/CliWrap.Tests.Dummy.csproj new file mode 100644 index 0000000..f7e2fa7 --- /dev/null +++ b/src/CliWrap.Tests.Dummy/CliWrap.Tests.Dummy.csproj @@ -0,0 +1,15 @@ + + + + Exe + net8.0 + false + + + + + + + + + diff --git a/src/CliWrap.Tests.Dummy/Commands/EchoCommand.cs b/src/CliWrap.Tests.Dummy/Commands/EchoCommand.cs new file mode 100644 index 0000000..97aaced --- /dev/null +++ b/src/CliWrap.Tests.Dummy/Commands/EchoCommand.cs @@ -0,0 +1,27 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using CliFx; +using CliFx.Attributes; +using CliFx.Infrastructure; +using CliWrap.Tests.Dummy.Commands.Shared; + +namespace CliWrap.Tests.Dummy.Commands; + +[Command("echo")] +public class EchoCommand : ICommand +{ + [CommandParameter(0)] + public required IReadOnlyList Items { get; init; } + + [CommandOption("target")] + public OutputTarget Target { get; init; } = OutputTarget.StdOut; + + [CommandOption("separator")] + public string Separator { get; init; } = " "; + + public async ValueTask ExecuteAsync(IConsole console) + { + foreach (var writer in console.GetWriters(Target)) + await writer.WriteLineAsync(string.Join(Separator, Items)); + } +} diff --git a/src/CliWrap.Tests.Dummy/Commands/EchoStdInCommand.cs b/src/CliWrap.Tests.Dummy/Commands/EchoStdInCommand.cs new file mode 100644 index 0000000..54459b6 --- /dev/null +++ b/src/CliWrap.Tests.Dummy/Commands/EchoStdInCommand.cs @@ -0,0 +1,39 @@ +using System; +using System.Buffers; +using System.Threading.Tasks; +using CliFx; +using CliFx.Attributes; +using CliFx.Infrastructure; +using CliWrap.Tests.Dummy.Commands.Shared; + +namespace CliWrap.Tests.Dummy.Commands; + +[Command("echo stdin")] +public class EchoStdInCommand : ICommand +{ + [CommandOption("target")] + public OutputTarget Target { get; init; } = OutputTarget.StdOut; + + [CommandOption("length")] + public long Length { get; init; } = long.MaxValue; + + public async ValueTask ExecuteAsync(IConsole console) + { + using var buffer = MemoryPool.Shared.Rent(81920); + + var totalBytesRead = 0L; + while (totalBytesRead < Length) + { + var bytesWanted = (int)Math.Min(buffer.Memory.Length, Length - totalBytesRead); + + var bytesRead = await console.Input.BaseStream.ReadAsync(buffer.Memory[..bytesWanted]); + if (bytesRead <= 0) + break; + + foreach (var writer in console.GetWriters(Target)) + await writer.BaseStream.WriteAsync(buffer.Memory[..bytesRead]); + + totalBytesRead += bytesRead; + } + } +} diff --git a/src/CliWrap.Tests.Dummy/Commands/EnvironmentCommand.cs b/src/CliWrap.Tests.Dummy/Commands/EnvironmentCommand.cs new file mode 100644 index 0000000..5fef363 --- /dev/null +++ b/src/CliWrap.Tests.Dummy/Commands/EnvironmentCommand.cs @@ -0,0 +1,21 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using CliFx; +using CliFx.Attributes; +using CliFx.Infrastructure; + +namespace CliWrap.Tests.Dummy.Commands; + +[Command("env")] +public class EnvironmentCommand : ICommand +{ + [CommandParameter(0)] + public IReadOnlyList Names { get; init; } = Array.Empty(); + + public async ValueTask ExecuteAsync(IConsole console) + { + foreach (var name in Names) + await console.Output.WriteLineAsync(Environment.GetEnvironmentVariable(name)); + } +} diff --git a/src/CliWrap.Tests.Dummy/Commands/ExitCommand.cs b/src/CliWrap.Tests.Dummy/Commands/ExitCommand.cs new file mode 100644 index 0000000..a2a5076 --- /dev/null +++ b/src/CliWrap.Tests.Dummy/Commands/ExitCommand.cs @@ -0,0 +1,22 @@ +using System.Threading.Tasks; +using CliFx; +using CliFx.Attributes; +using CliFx.Exceptions; +using CliFx.Infrastructure; + +namespace CliWrap.Tests.Dummy.Commands; + +[Command("exit")] +public class ExitCommand : ICommand +{ + [CommandParameter(0)] + public int ExitCode { get; init; } + + public ValueTask ExecuteAsync(IConsole console) + { + if (ExitCode != 0) + throw new CommandException($"Exit code set to {ExitCode}", ExitCode); + + return default; + } +} diff --git a/src/CliWrap.Tests.Dummy/Commands/GenerateBinaryCommand.cs b/src/CliWrap.Tests.Dummy/Commands/GenerateBinaryCommand.cs new file mode 100644 index 0000000..20c6200 --- /dev/null +++ b/src/CliWrap.Tests.Dummy/Commands/GenerateBinaryCommand.cs @@ -0,0 +1,43 @@ +using System; +using System.Buffers; +using System.Threading.Tasks; +using CliFx; +using CliFx.Attributes; +using CliFx.Infrastructure; +using CliWrap.Tests.Dummy.Commands.Shared; + +namespace CliWrap.Tests.Dummy.Commands; + +[Command("generate binary")] +public class GenerateBinaryCommand : ICommand +{ + // Tests rely on the random seed being fixed + private readonly Random _random = new(1234567); + + [CommandOption("target")] + public OutputTarget Target { get; init; } = OutputTarget.StdOut; + + [CommandOption("length")] + public long Length { get; init; } = 100_000; + + [CommandOption("buffer")] + public int BufferSize { get; init; } = 1024; + + public async ValueTask ExecuteAsync(IConsole console) + { + using var buffer = MemoryPool.Shared.Rent(BufferSize); + + var totalBytesGenerated = 0L; + while (totalBytesGenerated < Length) + { + _random.NextBytes(buffer.Memory.Span); + + var bytesWanted = (int)Math.Min(buffer.Memory.Length, Length - totalBytesGenerated); + + foreach (var writer in console.GetWriters(Target)) + await writer.BaseStream.WriteAsync(buffer.Memory[..bytesWanted]); + + totalBytesGenerated += bytesWanted; + } + } +} diff --git a/src/CliWrap.Tests.Dummy/Commands/GenerateTextCommand.cs b/src/CliWrap.Tests.Dummy/Commands/GenerateTextCommand.cs new file mode 100644 index 0000000..603c211 --- /dev/null +++ b/src/CliWrap.Tests.Dummy/Commands/GenerateTextCommand.cs @@ -0,0 +1,43 @@ +using System; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using CliFx; +using CliFx.Attributes; +using CliFx.Infrastructure; +using CliWrap.Tests.Dummy.Commands.Shared; + +namespace CliWrap.Tests.Dummy.Commands; + +[Command("generate text")] +public class GenerateTextCommand : ICommand +{ + // Tests rely on the random seed being fixed + private readonly Random _random = new(1234567); + private readonly char[] _allowedChars = Enumerable.Range(32, 94).Select(i => (char)i).ToArray(); + + [CommandOption("target")] + public OutputTarget Target { get; init; } = OutputTarget.StdOut; + + [CommandOption("length")] + public int Length { get; init; } = 100_000; + + [CommandOption("lines")] + public int LinesCount { get; init; } = 1; + + public async ValueTask ExecuteAsync(IConsole console) + { + for (var line = 0; line < LinesCount; line++) + { + var buffer = new StringBuilder(Length); + + for (var i = 0; i < Length; i++) + { + buffer.Append(_allowedChars[_random.Next(0, _allowedChars.Length)]); + } + + foreach (var writer in console.GetWriters(Target)) + await writer.WriteLineAsync(buffer.ToString()); + } + } +} diff --git a/src/CliWrap.Tests.Dummy/Commands/LengthStdInCommand.cs b/src/CliWrap.Tests.Dummy/Commands/LengthStdInCommand.cs new file mode 100644 index 0000000..22eb14b --- /dev/null +++ b/src/CliWrap.Tests.Dummy/Commands/LengthStdInCommand.cs @@ -0,0 +1,29 @@ +using System.Buffers; +using System.Globalization; +using System.Threading.Tasks; +using CliFx; +using CliFx.Attributes; +using CliFx.Infrastructure; + +namespace CliWrap.Tests.Dummy.Commands; + +[Command("length stdin")] +public class LengthStdInCommand : ICommand +{ + public async ValueTask ExecuteAsync(IConsole console) + { + using var buffer = MemoryPool.Shared.Rent(81920); + + var totalBytesRead = 0L; + while (true) + { + var bytesRead = await console.Input.BaseStream.ReadAsync(buffer.Memory); + if (bytesRead <= 0) + break; + + totalBytesRead += bytesRead; + } + + await console.Output.WriteLineAsync(totalBytesRead.ToString(CultureInfo.InvariantCulture)); + } +} diff --git a/src/CliWrap.Tests.Dummy/Commands/Shared/OutputTarget.cs b/src/CliWrap.Tests.Dummy/Commands/Shared/OutputTarget.cs new file mode 100644 index 0000000..47dfe65 --- /dev/null +++ b/src/CliWrap.Tests.Dummy/Commands/Shared/OutputTarget.cs @@ -0,0 +1,25 @@ +using System; +using System.Collections.Generic; +using CliFx.Infrastructure; + +namespace CliWrap.Tests.Dummy.Commands.Shared; + +[Flags] +public enum OutputTarget +{ + StdOut = 1, + StdErr = 2, + All = StdOut | StdErr +} + +internal static class OutputTargetExtensions +{ + public static IEnumerable GetWriters(this IConsole console, OutputTarget target) + { + if (target.HasFlag(OutputTarget.StdOut)) + yield return console.Output; + + if (target.HasFlag(OutputTarget.StdErr)) + yield return console.Error; + } +} diff --git a/src/CliWrap.Tests.Dummy/Commands/SleepCommand.cs b/src/CliWrap.Tests.Dummy/Commands/SleepCommand.cs new file mode 100644 index 0000000..64bdc3d --- /dev/null +++ b/src/CliWrap.Tests.Dummy/Commands/SleepCommand.cs @@ -0,0 +1,32 @@ +using System; +using System.Threading.Tasks; +using CliFx; +using CliFx.Attributes; +using CliFx.Infrastructure; + +namespace CliWrap.Tests.Dummy.Commands; + +[Command("sleep")] +public class SleepCommand : ICommand +{ + [CommandParameter(0)] + public TimeSpan Duration { get; init; } = TimeSpan.FromSeconds(1); + + public async ValueTask ExecuteAsync(IConsole console) + { + var cancellationToken = console.RegisterCancellationHandler(); + + try + { + await console.Output.WriteLineAsync($"Sleeping for {Duration}..."); + await Task.Delay(Duration, cancellationToken); + } + catch (OperationCanceledException) + { + await console.Output.WriteLineAsync("Canceled."); + return; + } + + await console.Output.WriteLineAsync("Done."); + } +} diff --git a/src/CliWrap.Tests.Dummy/Commands/WorkingDirectoryCommand.cs b/src/CliWrap.Tests.Dummy/Commands/WorkingDirectoryCommand.cs new file mode 100644 index 0000000..8bf9524 --- /dev/null +++ b/src/CliWrap.Tests.Dummy/Commands/WorkingDirectoryCommand.cs @@ -0,0 +1,14 @@ +using System.IO; +using System.Threading.Tasks; +using CliFx; +using CliFx.Attributes; +using CliFx.Infrastructure; + +namespace CliWrap.Tests.Dummy.Commands; + +[Command("cwd")] +public class WorkingDirectoryCommand : ICommand +{ + public async ValueTask ExecuteAsync(IConsole console) => + await console.Output.WriteLineAsync(Directory.GetCurrentDirectory()); +} diff --git a/src/CliWrap.Tests.Dummy/Program.cs b/src/CliWrap.Tests.Dummy/Program.cs new file mode 100644 index 0000000..76faa28 --- /dev/null +++ b/src/CliWrap.Tests.Dummy/Program.cs @@ -0,0 +1,32 @@ +using System; +using System.IO; +using System.Reflection; +using System.Runtime.InteropServices; +using System.Threading.Tasks; +using CliFx; + +namespace CliWrap.Tests.Dummy; + +public static class Program +{ + // Path to the apphost + public static string FilePath { get; } = + Path.ChangeExtension( + Assembly.GetExecutingAssembly().Location, + RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? "exe" : null + ); + + public static async Task Main(string[] args) + { + // Make sure color codes are not produced because we rely on the output in tests + Environment.SetEnvironmentVariable( + "DOTNET_SYSTEM_CONSOLE_ALLOW_ANSI_COLOR_REDIRECTION", + "false" + ); + + return await new CliApplicationBuilder() + .AddCommandsFromThisAssembly() + .Build() + .RunAsync(args); + } +}