diff --git a/.github/workflows/auto-approve.yml b/.github/workflows/auto-approve.yml new file mode 100644 index 0000000..dc707a7 --- /dev/null +++ b/.github/workflows/auto-approve.yml @@ -0,0 +1,13 @@ +name: Auto approve + +on: + pull_request_target + +jobs: + auto-approve: + runs-on: ubuntu-latest + steps: + - uses: hmarr/auto-approve-action@v2 + if: github.actor == 'Gitii' + with: + github-token: "${{ secrets.GITHUB_TOKEN }}" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 39e96c3..c55a24c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -7,7 +7,7 @@ on: - main jobs: - build: + test: runs-on: windows-latest steps: - name: Checkout @@ -30,3 +30,40 @@ jobs: run: dotnet build --configuration Release - name: Test run: dotnet test --configuration Release --no-build --filter TestCategory!=Integration + - name: Generate coverage + run: ./generate-coverage.cmd + - name: Upload code coverage results + uses: actions/upload-artifact@v2 + with: + name: code-coverage-report + path: CoverageResults + + verify: + runs-on: ubuntu-latest + needs: test + steps: + - name: Checkout + uses: actions/checkout@v2 + - name: Download code coverage resilts + uses: actions/download-artifact@v2 + with: + name: code-coverage-report + path: CoverageResults + - name: Code Coverage Summary Report + uses: irongut/CodeCoverageSummary@v1.2.0 + with: + filename: CoverageResults/coverage.cobertura.xml + badge: true + fail_below_min: true + format: markdown + hide_branch_rate: false + hide_complexity: true + indicators: true + output: both + thresholds: '53 44' + - name: Add Coverage PR Comment + uses: marocchino/sticky-pull-request-comment@v2 + if: github.event_name == 'pull_request' + with: + recreate: true + path: code-coverage-results.md diff --git a/.gitignore b/.gitignore index 056b3b5..f1d86b2 100644 --- a/.gitignore +++ b/.gitignore @@ -146,6 +146,7 @@ _TeamCity* coverage*.json coverage*.xml coverage*.info +CoverageReport # Visual Studio code coverage results *.coverage diff --git a/Community.Wsl.Sdk.Tests/BlockingReadOnlyStream.cs b/Community.Wsl.Sdk.Tests/BlockingReadOnlyStream.cs new file mode 100644 index 0000000..b832de6 --- /dev/null +++ b/Community.Wsl.Sdk.Tests/BlockingReadOnlyStream.cs @@ -0,0 +1,45 @@ +using System; +using System.Diagnostics.CodeAnalysis; +using System.IO; +using System.Threading; + +namespace Community.Wsl.Sdk.Tests; + +[ExcludeFromCodeCoverage] +public class BlockingReadOnlyStream : Stream +{ + public override void Flush() { } + + public override int Read(byte[] buffer, int offset, int count) + { + Thread.Sleep(5000); + return 0; + } + + public override long Seek(long offset, SeekOrigin origin) + { + throw new Exception(); + } + + public override void SetLength(long value) + { + throw new NotSupportedException(); + } + + public override void Write(byte[] buffer, int offset, int count) + { + throw new NotSupportedException(); + } + + public override bool CanRead { get; } = true; + public override bool CanSeek { get; } = false; + public override bool CanWrite { get; } = false; + + public override long Length => throw new Exception(); + + public override long Position + { + get { throw new Exception(); } + set { throw new Exception(); } + } +} diff --git a/Community.Wsl.Sdk.Tests/CommandExecutionOptionsTests.cs b/Community.Wsl.Sdk.Tests/CommandExecutionOptionsTests.cs new file mode 100644 index 0000000..5dc1689 --- /dev/null +++ b/Community.Wsl.Sdk.Tests/CommandExecutionOptionsTests.cs @@ -0,0 +1,35 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using FluentAssertions; +using NUnit.Framework; + +namespace Community.Wsl.Sdk.Tests; + +public class CommandExecutionOptionsTests +{ + [Test] + public void Constructor_ShouldEqualKnownValuesTests() + { + var ceo = new CommandExecutionOptions() + { + FailOnNegativeExitCode = true, + StdErrDataProcessingMode = DataProcessingMode.External, + StdInDataProcessingMode = DataProcessingMode.Binary, + StdoutDataProcessingMode = DataProcessingMode.String, + StderrEncoding = Encoding.ASCII, + StdinEncoding = Encoding.Default, + StdoutEncoding = Encoding.Latin1 + }; + + ceo.FailOnNegativeExitCode.Should().BeTrue(); + ceo.StdErrDataProcessingMode.Should().Be(DataProcessingMode.External); + ceo.StdInDataProcessingMode.Should().Be(DataProcessingMode.Binary); + ceo.StdoutDataProcessingMode.Should().Be(DataProcessingMode.String); + ceo.StderrEncoding.Should().BeSameAs(Encoding.ASCII); + ceo.StdinEncoding.Should().BeSameAs(Encoding.Default); + ceo.StdoutEncoding.Should().BeSameAs(Encoding.Latin1); + } +} diff --git a/Community.Wsl.Sdk.Tests/CommandResultTests.cs b/Community.Wsl.Sdk.Tests/CommandResultTests.cs new file mode 100644 index 0000000..dcdc5c7 --- /dev/null +++ b/Community.Wsl.Sdk.Tests/CommandResultTests.cs @@ -0,0 +1,31 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using FluentAssertions; +using NUnit.Framework; + +namespace Community.Wsl.Sdk.Tests; + +public class CommandResultTests +{ + [Test] + public void Constructor_ShouldEqualKnownValuesTests() + { + var cr = new CommandResult() + { + ExitCode = 0, + Stderr = "a", + StderrData = new byte[] { 1 }, + Stdout = "b", + StdoutData = new byte[] { 2 } + }; + + cr.ExitCode.Should().Be(0); + cr.Stderr.Should().Be("a"); + cr.StderrData.Should().Equal(1); + cr.Stdout.Should().Be("b"); + cr.StdoutData.Should().Equal(2); + } +} diff --git a/Community.Wsl.Sdk.Tests/CommandTests.cs b/Community.Wsl.Sdk.Tests/CommandTests.cs new file mode 100644 index 0000000..7ba67f9 --- /dev/null +++ b/Community.Wsl.Sdk.Tests/CommandTests.cs @@ -0,0 +1,286 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using Community.Wsl.Sdk.Strategies.Api; +using Community.Wsl.Sdk.Strategies.Commands; +using FakeItEasy; +using FluentAssertions; +using NUnit.Framework; + +namespace Community.Wsl.Sdk.Tests; + +public class CommandTests +{ + public Command CreateCommand( + string distroName, + string command, + string[] arguments, + CommandExecutionOptions options, + bool asRoot = false, + bool shellExecute = false + ) + { + var io = A.Fake(); + var env = A.Fake(); + var pm = A.Fake(); + + return new Command( + distroName, + command, + arguments, + options, + asRoot, + shellExecute, + env, + io, + pm + ); + } + + [Test] + public void Constructor_ShouldHaveDefaultValues() + { + var cmd = CreateCommand("dn", "", Array.Empty(), new CommandExecutionOptions()); + + cmd.HasExited.Should().BeFalse(); + cmd.HasWaited.Should().BeFalse(); + cmd.IsDisposed.Should().BeFalse(); + cmd.IsStarted.Should().BeFalse(); + } + + [TestCase(DataProcessingMode.Binary)] + [TestCase(DataProcessingMode.String)] + public void Constructor_ShouldFailWhenOptionsAreInvalid(DataProcessingMode mode) + { + var call = () => + CreateCommand( + "dn", + "", + Array.Empty(), + new CommandExecutionOptions() { StdInDataProcessingMode = mode, } + ); + + call.Should().Throw("StandardInput can only be dropped or external."); + } + + [TestCase(true, false)] + [TestCase(false, false)] + public void Start_ShouldCreateProcessAndStartIt(bool isRoot, bool shellExecute) + { + var distroName = "distro"; + var command = "command"; + var arguments = new string[] { "args" }; + var options = new CommandExecutionOptions(); + + var io = A.Fake(); + var env = A.Fake(); + var pm = A.Fake(); + + var p = A.Fake(); + + ProcessStartInfo actualStartInfo = new ProcessStartInfo(); + A.CallTo(() => pm.Start(A._)) + .Invokes((psi) => actualStartInfo = psi.GetArgument(0)!) + .Returns(p); + + var cmd = new Command( + distroName, + command, + arguments, + options, + isRoot, + shellExecute, + env, + io, + pm + ); + + var results = cmd.Start(); + + results.StandardOutput.Should().Be(StreamReader.Null); + results.StandardError.Should().Be(StreamReader.Null); + results.StandardInput.Should().Be(StreamWriter.Null); + + A.CallTo(() => pm.Start(A._)).MustHaveHappened(); + + var expectedArgs = new List() { "-d", distroName }; + + if (isRoot) + { + expectedArgs.Add("--user"); + expectedArgs.Add("root"); + } + + expectedArgs.Add(shellExecute ? "--" : "--exec"); + expectedArgs.Add(command); + expectedArgs.AddRange(arguments); + + actualStartInfo.ArgumentList.Should().BeEquivalentTo(expectedArgs); + + actualStartInfo.RedirectStandardOutput.Should().BeFalse(); + actualStartInfo.RedirectStandardError.Should().BeFalse(); + actualStartInfo.RedirectStandardInput.Should().BeFalse(); + + actualStartInfo.CreateNoWindow.Should().BeTrue(); + } + + [TestCase(DataProcessingMode.Drop, typeof(StreamNullReader))] + [TestCase(DataProcessingMode.Binary, typeof(StreamDataReader))] + [TestCase(DataProcessingMode.String, typeof(StreamStringReader))] + [TestCase(DataProcessingMode.External, typeof(StreamNullReader))] + public void Start_ShouldCreateStdoutStreams(DataProcessingMode mode, Type readerType) + { + var distroName = "distro"; + var command = "command"; + var arguments = new string[] { "args" }; + var options = new CommandExecutionOptions() { StdoutDataProcessingMode = mode }; + + var io = A.Fake(); + var env = A.Fake(); + var pm = A.Fake(); + + var p = A.Fake(); + A.CallTo(() => p.StandardOutput).Returns(new StreamReader(Stream.Null)); + + ProcessStartInfo actualStartInfo = new ProcessStartInfo(); + A.CallTo(() => pm.Start(A._)) + .Invokes((psi) => actualStartInfo = psi.GetArgument(0)!) + .Returns(p); + + var cmd = new Command(distroName, command, arguments, options, false, false, env, io, pm); + + var results = cmd.Start(); + + if (mode == DataProcessingMode.Drop) + { + actualStartInfo.RedirectStandardOutput.Should().BeFalse(); + results.StandardOutput.Should().Be(StreamReader.Null); + } + else + { + actualStartInfo.RedirectStandardOutput.Should().BeTrue(); + results.StandardOutput.Should().NotBe(StreamReader.Null); + } + + results.StandardError.Should().Be(StreamReader.Null); + results.StandardInput.Should().Be(StreamWriter.Null); + actualStartInfo.RedirectStandardError.Should().BeFalse(); + actualStartInfo.RedirectStandardInput.Should().BeFalse(); + + cmd.StdoutReader.Should().BeOfType(readerType); + } + + [TestCase(DataProcessingMode.Drop, typeof(StreamNullReader))] + [TestCase(DataProcessingMode.Binary, typeof(StreamDataReader))] + [TestCase(DataProcessingMode.String, typeof(StreamStringReader))] + [TestCase(DataProcessingMode.External, typeof(StreamNullReader))] + public void Start_ShouldCreateStderrStreams(DataProcessingMode mode, Type readerType) + { + var distroName = "distro"; + var command = "command"; + var arguments = new string[] { "args" }; + var options = new CommandExecutionOptions() { StdErrDataProcessingMode = mode }; + + var io = A.Fake(); + var env = A.Fake(); + var pm = A.Fake(); + + var p = A.Fake(); + + A.CallTo(() => p.StandardError).Returns(new StreamReader(Stream.Null)); + + ProcessStartInfo actualStartInfo = new ProcessStartInfo(); + A.CallTo(() => pm.Start(A._)) + .Invokes((psi) => actualStartInfo = psi.GetArgument(0)!) + .Returns(p); + + var cmd = new Command(distroName, command, arguments, options, false, false, env, io, pm); + + var results = cmd.Start(); + + if (mode == DataProcessingMode.Drop) + { + actualStartInfo.RedirectStandardError.Should().BeFalse(); + results.StandardError.Should().Be(StreamReader.Null); + } + else + { + actualStartInfo.RedirectStandardError.Should().BeTrue(); + results.StandardError.Should().NotBe(StreamReader.Null); + } + + results.StandardOutput.Should().Be(StreamReader.Null); + results.StandardInput.Should().Be(StreamWriter.Null); + actualStartInfo.RedirectStandardOutput.Should().BeFalse(); + actualStartInfo.RedirectStandardInput.Should().BeFalse(); + + cmd.StderrReader.Should().BeOfType(readerType); + } + + [TestCase(DataProcessingMode.Drop, typeof(StreamNullReader))] + [TestCase(DataProcessingMode.External, typeof(StreamNullReader))] + public void Start_ShouldCreateStdInStreams(DataProcessingMode mode, Type readerType) + { + var distroName = "distro"; + var command = "command"; + var arguments = new string[] { "args" }; + var options = new CommandExecutionOptions() { StdInDataProcessingMode = mode }; + + var io = A.Fake(); + var env = A.Fake(); + var pm = A.Fake(); + + var p = A.Fake(); + + A.CallTo(() => p.StandardInput).Returns(new StreamWriter(Stream.Null)); + + ProcessStartInfo actualStartInfo = new ProcessStartInfo(); + A.CallTo(() => pm.Start(A._)) + .Invokes((psi) => actualStartInfo = psi.GetArgument(0)!) + .Returns(p); + + var cmd = new Command(distroName, command, arguments, options, false, false, env, io, pm); + + var results = cmd.Start(); + + if (mode == DataProcessingMode.Drop) + { + actualStartInfo.RedirectStandardInput.Should().BeFalse(); + results.StandardInput.Should().Be(StreamWriter.Null); + } + else + { + actualStartInfo.RedirectStandardInput.Should().BeTrue(); + results.StandardInput.Should().NotBe(StreamWriter.Null); + } + + results.StandardOutput.Should().Be(StreamReader.Null); + results.StandardError.Should().Be(StreamReader.Null); + actualStartInfo.RedirectStandardOutput.Should().BeFalse(); + actualStartInfo.RedirectStandardError.Should().BeFalse(); + } + + [Test] + public void Start_ShouldFailWhenStartedTwice() + { + var distroName = "distro"; + var command = "command"; + var arguments = new string[] { "args" }; + var options = new CommandExecutionOptions(); + + var io = A.Fake(); + var env = A.Fake(); + var pm = A.Fake(); + + var cmd = new Command(distroName, command, arguments, options, false, false, env, io, pm); + + cmd.Start(); + + var call = () => cmd.Start(); + + cmd.IsStarted.Should().BeTrue(); + + call.Should().Throw("Command has already been started!"); + } +} diff --git a/Community.Wsl.Sdk.Tests/Community.Wsl.Sdk.Tests.csproj b/Community.Wsl.Sdk.Tests/Community.Wsl.Sdk.Tests.csproj index 7c802b7..b93db26 100644 --- a/Community.Wsl.Sdk.Tests/Community.Wsl.Sdk.Tests.csproj +++ b/Community.Wsl.Sdk.Tests/Community.Wsl.Sdk.Tests.csproj @@ -12,12 +12,19 @@ + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + - diff --git a/Community.Wsl.Sdk.Tests/DistroInfoTests.cs b/Community.Wsl.Sdk.Tests/DistroInfoTests.cs new file mode 100644 index 0000000..de69c95 --- /dev/null +++ b/Community.Wsl.Sdk.Tests/DistroInfoTests.cs @@ -0,0 +1,44 @@ +using System; +using Community.Wsl.Sdk.Strategies.Api; +using FluentAssertions; +using NUnit.Framework; + +namespace Community.Wsl.Sdk.Tests; + +public class DistroInfoTests +{ + [Test] + public void Constructor_ShouldEqualKnownInstanceTest() + { + var di = new DistroInfo() + { + DistroId = Guid.Empty, + BasePath = "path", + DefaultEnvironmentVariables = Array.Empty(), + DefaultUid = 0, + DistroFlags = DistroFlags.None, + DistroName = "name", + IsDefault = false, + KernelCommandLine = Array.Empty(), + WslVersion = 2 + }; + + di.DistroId.Should().Equals(Guid.Empty); + di.BasePath.Should().Be("path"); + di.DefaultEnvironmentVariables.Should().BeEmpty(); + di.DefaultUid.Should().Be(0); + di.DistroFlags.Should().Be(DistroFlags.None); + di.DistroName.Should().Be("name"); + di.IsDefault.Should().BeFalse(); + di.KernelCommandLine.Should().BeEmpty(); + di.WslVersion.Should().Be(2); + } + + [Test] + public void ToString_ShouldEqualKnownStringTests() + { + var di = new DistroInfo() { DistroName = "test", DistroId = Guid.NewGuid(), }; + + di.ToString().Should().Be($"{di.DistroName} [{di.DistroId}]"); + } +} diff --git a/Community.Wsl.Sdk.Tests/IntegrationsTests/ManagedCommandTests.cs b/Community.Wsl.Sdk.Tests/IntegrationsTests/ManagedCommandTests.cs deleted file mode 100644 index f0dded4..0000000 --- a/Community.Wsl.Sdk.Tests/IntegrationsTests/ManagedCommandTests.cs +++ /dev/null @@ -1,143 +0,0 @@ -using System; -using System.Threading.Tasks; -using Community.Wsl.Sdk.Strategies.Api; -using Community.Wsl.Sdk.Strategies.Commands; -using FluentAssertions; -using NUnit.Framework; - -namespace Community.Wsl.Sdk.Tests.IntegrationsTests; - -[TestFixture(Category = "Integration")] -internal class ManagedCommandTests -{ - private string _distroName = String.Empty; - - [SetUp] - public void Setup() - { - IWslApi api = new WslApi(); - _distroName = api.GetDefaultDistro()!.Value.DistroName; - } - - [Test] - public void Test_expect_stdout_to_equal_constant() - { - var cmd = new Command( - _distroName, - "echo", - new string[] { "-n", "test" }, - new CommandExecutionOptions() { StdoutDataProcessingMode = DataProcessingMode.String } - ); - - cmd.Start(); - var result = cmd.WaitAndGetResults(); - - result.Stdout.Should().BeEquivalentTo("test"); - result.StdoutData.Should().BeNull(); - - result.Stderr.Should().BeNull(); - result.StderrData.Should().BeNull(); - } - - [Test] - public void Test_expect_stderr_to_equal_constant() - { - var cmd = new Command( - _distroName, - "echo", - new string[] { "-n", "test", "1>&2" }, - new CommandExecutionOptions() { StdErrDataProcessingMode = DataProcessingMode.String }, - shellExecute: true - ); - - cmd.Start(); - var result = cmd.WaitAndGetResults(); - - result.Stderr.Should().BeEquivalentTo("test"); - result.StderrData.Should().BeNull(); - - result.Stdout.Should().BeNull(); - result.StdoutData.Should().BeNull(); - } - - [Test] - public void Test_expect_stdout_to_equal_stdin() - { - var cmd = new Command( - _distroName, - "read", - new string[] { "-n", "4" }, - new CommandExecutionOptions() - { - StdoutDataProcessingMode = DataProcessingMode.String, - StdErrDataProcessingMode = DataProcessingMode.String, - StdInDataProcessingMode = DataProcessingMode.External - }, - shellExecute: true - ); - - var pipes = cmd.Start(); - - pipes.StandardInput.Write("test"); - - var result = cmd.WaitAndGetResults(); - - result.Stdout.Should().BeEquivalentTo("test"); - result.StdoutData.Should().BeNull(); - - result.Stderr.Should().BeEmpty(); - result.StderrData.Should().BeNull(); - } - - [Test] - public async Task Test_async_waitAsync() - { - var cmd = new Command( - _distroName, - "echo", - new string[] { "-n", "test" }, - new CommandExecutionOptions() { StdoutDataProcessingMode = DataProcessingMode.String } - ); - - cmd.Start(); - var result = await cmd.WaitAndGetResultsAsync().ConfigureAwait(false); - - result.Stdout.Should().BeEquivalentTo("test"); - result.StdoutData.Should().BeNull(); - - result.Stderr.Should().BeNull(); - result.StderrData.Should().BeNull(); - } - - [Test] - public void Test_exit_code() - { - var cmd = new Command( - _distroName, - "this_command_doesnt_exit", - new[] { "-n", "test" }, - new CommandExecutionOptions() { FailOnNegativeExitCode = false } - ); - - cmd.Start(); - var result = cmd.WaitAndGetResults(); - - result.ExitCode.Should().NotBe(0); - } - - [Test] - public async Task Test_exit_code_asyncAsync() - { - var cmd = new Command( - _distroName, - "this_command_doesnt_exit", - new[] { "-n", "test" }, - new CommandExecutionOptions() { FailOnNegativeExitCode = false } - ); - - cmd.Start(); - var result = await cmd.WaitAndGetResultsAsync().ConfigureAwait(false); - - result.ExitCode.Should().NotBe(0); - } -} diff --git a/Community.Wsl.Sdk.Tests/StreamDataReaderTests.cs b/Community.Wsl.Sdk.Tests/StreamDataReaderTests.cs new file mode 100644 index 0000000..565a19b --- /dev/null +++ b/Community.Wsl.Sdk.Tests/StreamDataReaderTests.cs @@ -0,0 +1,113 @@ +using System; +using System.IO; +using System.Threading.Tasks; +using Community.Wsl.Sdk.Strategies.Commands; +using FakeItEasy; +using FluentAssertions; +using NUnit.Framework; + +namespace Community.Wsl.Sdk.Tests; + +public class StreamDataReaderTests +{ + [TestCase(true)] + [TestCase(false)] + public void CopyResultTo_ShouldHaveEmptyOutputTests(bool isStdOut) + { + var reader = new StreamReader(new MemoryStream(new byte[] { 1, 2 })); + + var snr = new StreamDataReader(reader); + + snr.Fetch(); + snr.Wait(); + + CommandResult r = new CommandResult(); + + snr.CopyResultTo(ref r, isStdOut); + + if (isStdOut) + { + r.Stdout.Should().BeNull(); + r.StdoutData.Should().Equal(1, 2); + r.Stderr.Should().BeNull(); + r.StderrData.Should().BeNull(); + r.ExitCode.Should().Be(0); + } + else + { + r.Stdout.Should().BeNull(); + r.StdoutData.Should().BeNull(); + r.Stderr.Should().BeNull(); + r.StderrData.Should().Equal(1, 2); + r.ExitCode.Should().Be(0); + } + } + + [TestCase(true)] + [TestCase(false)] + public async Task CopyResultTo_ShouldHaveEmptyOutputTestsAsync(bool isStdOut) + { + var reader = new StreamReader(new MemoryStream(new byte[] { 1, 2 })); + + var snr = new StreamDataReader(reader); + + snr.Fetch(); + await snr.WaitAsync().ConfigureAwait(false); + + CommandResult r = new CommandResult(); + + snr.CopyResultTo(ref r, isStdOut); + + if (isStdOut) + { + r.Stdout.Should().BeNull(); + r.StdoutData.Should().Equal(1, 2); + r.Stderr.Should().BeNull(); + r.StderrData.Should().BeNull(); + r.ExitCode.Should().Be(0); + } + else + { + r.Stdout.Should().BeNull(); + r.StdoutData.Should().BeNull(); + r.Stderr.Should().BeNull(); + r.StderrData.Should().Equal(1, 2); + r.ExitCode.Should().Be(0); + } + } + + [Test] + public void Fetch_ShouldFailWhenFetchTwiceTests() + { + var srn = new StreamDataReader(new StreamReader(Stream.Null)); + srn.Fetch(); + + var call = () => srn.Fetch(); + + call.Should().Throw("Already started fetching!"); + } + + [Test] + public void CopyResultTo_ShouldFailWhenNotFetchedTests() + { + var srn = new StreamDataReader(new StreamReader(Stream.Null)); + + CommandResult _ = new CommandResult(); + var call = () => srn.CopyResultTo(ref _, false); + + call.Should().Throw("Data hasn't been fetched, yet!"); + } + + [Test] + public void CopyResultTo_ShouldFailWhenNotWaitedTests() + { + var stream = new BlockingReadOnlyStream(); + var srn = new StreamDataReader(new StreamReader(stream)); + srn.Fetch(); + + CommandResult _ = new CommandResult(); + var call = () => srn.CopyResultTo(ref _, false); + + call.Should().Throw("Fetching hasn't been finished, yet!"); + } +} diff --git a/Community.Wsl.Sdk.Tests/StreamNullReaderTests.cs b/Community.Wsl.Sdk.Tests/StreamNullReaderTests.cs new file mode 100644 index 0000000..41dd479 --- /dev/null +++ b/Community.Wsl.Sdk.Tests/StreamNullReaderTests.cs @@ -0,0 +1,53 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Community.Wsl.Sdk.Strategies.Commands; +using FluentAssertions; +using NUnit.Framework; + +namespace Community.Wsl.Sdk.Tests; + +public class StreamNullReaderTests +{ + [TestCase(true)] + [TestCase(false)] + public void CopyResultTo_ShouldHaveEmptyOutputTests(bool isStdOut) + { + var snr = new StreamNullReader(); + + snr.Wait(); + snr.Fetch(); + + CommandResult r = new CommandResult(); + + snr.CopyResultTo(ref r, isStdOut); + + r.Stdout.Should().BeNull(); + r.StdoutData.Should().BeNull(); + r.Stderr.Should().BeNull(); + r.StderrData.Should().BeNull(); + r.ExitCode.Should().Be(0); + } + + [TestCase(true)] + [TestCase(false)] + public async Task CopyResultTo_ShouldHaveEmptyOutputTestsAsync(bool isStdOut) + { + var snr = new StreamNullReader(); + + await snr.WaitAsync().ConfigureAwait(false); + snr.Fetch(); + + CommandResult r = new CommandResult(); + + snr.CopyResultTo(ref r, isStdOut); + + r.Stdout.Should().BeNull(); + r.StdoutData.Should().BeNull(); + r.Stderr.Should().BeNull(); + r.StderrData.Should().BeNull(); + r.ExitCode.Should().Be(0); + } +} diff --git a/Community.Wsl.Sdk.Tests/StreamStringReaderTests.cs b/Community.Wsl.Sdk.Tests/StreamStringReaderTests.cs new file mode 100644 index 0000000..6884c1d --- /dev/null +++ b/Community.Wsl.Sdk.Tests/StreamStringReaderTests.cs @@ -0,0 +1,44 @@ +using System; +using System.IO; +using System.Threading.Tasks; +using Community.Wsl.Sdk.Strategies.Commands; +using FluentAssertions; +using NUnit.Framework; + +namespace Community.Wsl.Sdk.Tests; + +public class StreamStringReaderTests +{ + [TestCase(true)] + [TestCase(false)] + public void CopyResultTo_ShouldHaveEmptyOutputTests(bool isStdOut) + { + var reader = new StreamReader(new MemoryStream(new byte[] { (byte)'a' })); + + var snr = new StreamStringReader(reader); + + snr.Fetch(); + snr.Wait(); + + CommandResult r = new CommandResult(); + + snr.CopyResultTo(ref r, isStdOut); + + if (isStdOut) + { + r.Stdout.Should().Be("a"); + r.StdoutData.Should().BeNull(); + r.Stderr.Should().BeNull(); + r.StderrData.Should().BeNull(); + r.ExitCode.Should().Be(0); + } + else + { + r.Stdout.Should().BeNull(); + r.StdoutData.Should().BeNull(); + r.Stderr.Should().Be("a"); + r.StderrData.Should().BeNull(); + r.ExitCode.Should().Be(0); + } + } +} diff --git a/Community.Wsl.Sdk.Tests/UnitTests/ManagedCommandTests.cs b/Community.Wsl.Sdk.Tests/UnitTests/ManagedCommandTests.cs deleted file mode 100644 index a89b07c..0000000 --- a/Community.Wsl.Sdk.Tests/UnitTests/ManagedCommandTests.cs +++ /dev/null @@ -1,89 +0,0 @@ -using System; -using System.Diagnostics; -using System.IO; -using Community.Wsl.Sdk.Strategies.Api; -using Community.Wsl.Sdk.Strategies.Commands; -using FakeItEasy; -using FluentAssertions; -using NUnit.Framework; - -namespace Community.Wsl.Sdk.Tests.UnitTests; - -public class ManagedCommandTests -{ - public Command CreateCommand( - string distroName, - string command, - string[] arguments, - CommandExecutionOptions options, - bool asRoot = false, - bool shellExecute = false - ) - { - var io = A.Fake(); - var env = A.Fake(); - var pm = A.Fake(); - - return new Command( - distroName, - command, - arguments, - options, - asRoot, - shellExecute, - env, - io, - pm - ); - } - - [Test] - public void Test_Constructor() - { - var cmd = CreateCommand("dn", "", Array.Empty(), new CommandExecutionOptions()); - - cmd.HasExited.Should().BeFalse(); - cmd.HasWaited.Should().BeFalse(); - cmd.IsDisposed.Should().BeFalse(); - cmd.IsStarted.Should().BeFalse(); - } - - [Test] - public void Test_Start() - { - var distroName = "distro"; - var command = "command"; - var arguments = new string[] { "args" }; - var options = new CommandExecutionOptions(); - - var io = A.Fake(); - var env = A.Fake(); - var pm = A.Fake(); - - var p = A.Fake(); - - ProcessStartInfo actualStartInfo = new ProcessStartInfo(); - A.CallTo(() => pm.Start(A._)) - .Invokes((psi) => actualStartInfo = psi.GetArgument(0)!) - .Returns(p); - - var cmd = new Command(distroName, command, arguments, options, false, false, env, io, pm); - - var results = cmd.Start(); - - results.StandardOutput.Should().Be(StreamReader.Null); - results.StandardError.Should().Be(StreamReader.Null); - results.StandardInput.Should().Be(StreamWriter.Null); - - A.CallTo(() => pm.Start(A._)).MustHaveHappened(); - - actualStartInfo.ArgumentList - .Should() - .BeEquivalentTo("-d", distroName, "--exec", command, arguments[0]); - actualStartInfo.RedirectStandardOutput.Should().BeFalse(); - actualStartInfo.RedirectStandardError.Should().BeFalse(); - actualStartInfo.RedirectStandardInput.Should().BeFalse(); - - actualStartInfo.CreateNoWindow.Should().BeTrue(); - } -} diff --git a/Community.Wsl.Sdk.Tests/UnitTests/Win32EnvironmentTests.cs b/Community.Wsl.Sdk.Tests/UnitTests/Win32EnvironmentTests.cs deleted file mode 100644 index bcd8ef2..0000000 --- a/Community.Wsl.Sdk.Tests/UnitTests/Win32EnvironmentTests.cs +++ /dev/null @@ -1,46 +0,0 @@ -using System; -using Community.Wsl.Sdk.Strategies.Api; -using FluentAssertions; -using NUnit.Framework; - -namespace Community.Wsl.Sdk.Tests.UnitTests -{ - public class Win32EnvironmentTests - { - [Test] - public void Test_Is64BitOperatingSystem() - { - var env = new Win32Environment(); - - env.Is64BitOperatingSystem.Should().Be(Environment.Is64BitOperatingSystem); - } - - [Test] - public void Test_Is64BitProcess() - { - var env = new Win32Environment(); - - env.Is64BitProcess.Should().Be(Environment.Is64BitProcess); - } - - [Test] - public void Test_OSVersion() - { - var env = new Win32Environment(); - - env.OSVersion.Should().BeEquivalentTo(Environment.OSVersion); - } - - public static readonly Environment.SpecialFolder[] FolderNames = - Enum.GetValues(); - - [Test] - [TestCaseSource(nameof(FolderNames))] - public void Test_GetFolderPath(Environment.SpecialFolder folder) - { - var env = new Win32Environment(); - - env.GetFolderPath(folder).Should().BeEquivalentTo(Environment.GetFolderPath(folder)); - } - } -} \ No newline at end of file diff --git a/Community.Wsl.Sdk.Tests/UnitTests/Win32RegistryKeyTests.cs b/Community.Wsl.Sdk.Tests/UnitTests/Win32RegistryKeyTests.cs deleted file mode 100644 index 8c237f8..0000000 --- a/Community.Wsl.Sdk.Tests/UnitTests/Win32RegistryKeyTests.cs +++ /dev/null @@ -1,93 +0,0 @@ -using System; -using System.Collections.Generic; -using Community.Wsl.Sdk.Strategies.Api; -using FluentAssertions; -using Microsoft.Win32; -using NUnit.Framework; - -namespace Community.Wsl.Sdk.Tests.UnitTests -{ - /// - /// NOTE: Actually these are integration tests because they rely on the actual windows registry. - /// The used hives and keys are well-known and should be stable on all (valid) windows-based test runner. - /// - public class Win32RegistryKeyTests - { - [Test] - public void Test_GetSubKeyNames() - { - var reg = new Win32RegistryKey(Registry.LocalMachine); - - var pubs = reg.OpenSubKey( - "Software\\Microsoft\\Windows\\CurrentVersion\\WINEVT\\Publishers" - ); - - var values = pubs.GetSubKeyNames(); - - Guid dummy; - values.Should().NotBeEmpty().And.OnlyContain((key) => Guid.TryParse(key, out dummy)); - } - - [Test] - public void Test_GetValue_string() - { - var reg = new Win32RegistryKey(Registry.CurrentUser); - - var console = reg.OpenSubKey("Console"); - - console.GetValue("FaceName").Should().NotBeNull(); - } - - [Test] - public void Test_GetValue_int() - { - var reg = new Win32RegistryKey(Registry.CurrentUser); - - var console = reg.OpenSubKey("Console"); - - console.GetValue("CursorSize").Should().BeGreaterThan(0); - } - - [Test] - public void Test_GetValue_guid() - { - var reg = new Win32RegistryKey(Registry.LocalMachine); - - var crypt = reg.OpenSubKey("SOFTWARE\\Microsoft\\Cryptography"); - - crypt.GetValue("MachineGuid").Should().NotBe(Guid.Empty); - } - - [Test] - public void Test_GetValue_not_existing_key() - { - var reg = new Win32RegistryKey(Registry.CurrentUser); - - var call = () => reg.GetValue("IDoNotExist"); - - call.Should().Throw(); - } - - [Test] - public void Test_GetValue_default_value() - { - var reg = new Win32RegistryKey(Registry.CurrentUser); - - var value = reg.GetValue("IDoNotExist", "foobar"); - - value.Should().BeEquivalentTo("foobar"); - } - - [Test] - public void Test_GetValue_unsupported_type() - { - var reg = new Win32RegistryKey(Registry.CurrentUser); - - var console = reg.OpenSubKey("Console"); - - var call = () => console.GetValue("CursorSize"); - - call.Should().Throw(); - } - } -} diff --git a/Community.Wsl.Sdk.Tests/Win32EnvironmentTests.cs b/Community.Wsl.Sdk.Tests/Win32EnvironmentTests.cs new file mode 100644 index 0000000..abf9d65 --- /dev/null +++ b/Community.Wsl.Sdk.Tests/Win32EnvironmentTests.cs @@ -0,0 +1,45 @@ +using System; +using Community.Wsl.Sdk.Strategies.Api; +using FluentAssertions; +using NUnit.Framework; + +namespace Community.Wsl.Sdk.Tests; + +public class Win32EnvironmentTests +{ + [Test] + public void Test_Is64BitOperatingSystem() + { + var env = new Win32Environment(); + + env.Is64BitOperatingSystem.Should().Be(Environment.Is64BitOperatingSystem); + } + + [Test] + public void Test_Is64BitProcess() + { + var env = new Win32Environment(); + + env.Is64BitProcess.Should().Be(Environment.Is64BitProcess); + } + + [Test] + public void Test_OSVersion() + { + var env = new Win32Environment(); + + env.OSVersion.Should().BeEquivalentTo(Environment.OSVersion); + } + + public static readonly Environment.SpecialFolder[] FolderNames = + Enum.GetValues(); + + [Test] + [TestCaseSource(nameof(FolderNames))] + public void Test_GetFolderPath(Environment.SpecialFolder folder) + { + var env = new Win32Environment(); + + env.GetFolderPath(folder).Should().BeEquivalentTo(Environment.GetFolderPath(folder)); + } +} diff --git a/Community.Wsl.Sdk.Tests/Win32RegistryKeyTests.cs b/Community.Wsl.Sdk.Tests/Win32RegistryKeyTests.cs new file mode 100644 index 0000000..8d67d58 --- /dev/null +++ b/Community.Wsl.Sdk.Tests/Win32RegistryKeyTests.cs @@ -0,0 +1,94 @@ +using System; +using System.Collections.Generic; +using System.Runtime.Versioning; +using Community.Wsl.Sdk.Strategies.Api; +using FluentAssertions; +using Microsoft.Win32; +using NUnit.Framework; + +namespace Community.Wsl.Sdk.Tests; + +/// +/// NOTE: Actually these are integration tests because they rely on the actual windows registry. +/// The used hives and keys are well-known and should be stable on all (valid) windows-based test runner. +/// +[SupportedOSPlatform("windows")] +public class Win32RegistryKeyTests +{ + [Test] + public void Test_GetSubKeyNames() + { + var reg = new Win32RegistryKey(Registry.LocalMachine); + + var pubs = reg.OpenSubKey( + "Software\\Microsoft\\Windows\\CurrentVersion\\WINEVT\\Publishers" + ); + + var values = pubs.GetSubKeyNames(); + + Guid dummy; + values.Should().NotBeEmpty().And.OnlyContain((key) => Guid.TryParse(key, out dummy)); + } + + [Test] + public void Test_GetValue_string() + { + var reg = new Win32RegistryKey(Registry.CurrentUser); + + var console = reg.OpenSubKey("Console"); + + console.GetValue("FaceName").Should().NotBeNull(); + } + + [Test] + public void Test_GetValue_int() + { + var reg = new Win32RegistryKey(Registry.CurrentUser); + + var console = reg.OpenSubKey("Console"); + + console.GetValue("CursorSize").Should().BeGreaterThan(0); + } + + [Test] + public void Test_GetValue_guid() + { + var reg = new Win32RegistryKey(Registry.LocalMachine); + + var crypt = reg.OpenSubKey("SOFTWARE\\Microsoft\\Cryptography"); + + crypt.GetValue("MachineGuid").Should().NotBe(Guid.Empty); + } + + [Test] + public void Test_GetValue_not_existing_key() + { + var reg = new Win32RegistryKey(Registry.CurrentUser); + + var call = () => reg.GetValue("IDoNotExist"); + + call.Should().Throw(); + } + + [Test] + public void Test_GetValue_default_value() + { + var reg = new Win32RegistryKey(Registry.CurrentUser); + + var value = reg.GetValue("IDoNotExist", "foobar"); + + value.Should().BeEquivalentTo("foobar"); + } + + [Test] + public void Test_GetValue_unsupported_type() + { + var reg = new Win32RegistryKey(Registry.CurrentUser); + + var console = reg.OpenSubKey("Console"); + + var call = () => console.GetValue("CursorSize"); + + call.Should().Throw(); + } +} diff --git a/Community.Wsl.Sdk.sln b/Community.Wsl.Sdk.sln index c0167c0..5998390 100644 --- a/Community.Wsl.Sdk.sln +++ b/Community.Wsl.Sdk.sln @@ -10,7 +10,11 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution .editorconfig = .editorconfig .gitignore = .gitignore .onsaveconfig = .onsaveconfig + .github\workflows\auto-approve.yml = .github\workflows\auto-approve.yml .github\workflows\ci.yml = .github\workflows\ci.yml + generate-coverage-report.cmd = generate-coverage-report.cmd + generate-coverage.cmd = generate-coverage.cmd + generate-coverage.sh = generate-coverage.sh License = License nuget.config = nuget.config README.md = README.md diff --git a/Community.Wsl.Sdk/DistroInfo.cs b/Community.Wsl.Sdk/DistroInfo.cs index 8a2fab5..50b1eba 100644 --- a/Community.Wsl.Sdk/DistroInfo.cs +++ b/Community.Wsl.Sdk/DistroInfo.cs @@ -4,7 +4,7 @@ namespace Community.Wsl.Sdk; /// -/// A model class that contains information. +/// A POD class that contains information about a wsl distribution. /// public readonly struct DistroInfo { @@ -43,10 +43,10 @@ public readonly struct DistroInfo /// public int DefaultUid { get; init; } - /// - /// Represents the default settings of the distribution. - /// - public DistroFlags DistroFlags { get; init; } + /// + /// Represents the default settings of the distribution. + /// + public DistroFlags DistroFlags { get; init; } /// /// Determine which version of the WSL runtime is configured to use. diff --git a/Community.Wsl.Sdk/IsExternalInit.cs b/Community.Wsl.Sdk/IsExternalInit.cs index 188260b..ec5ba38 100644 --- a/Community.Wsl.Sdk/IsExternalInit.cs +++ b/Community.Wsl.Sdk/IsExternalInit.cs @@ -1,9 +1,9 @@ -using System.ComponentModel; - -#if NETSTANDARD2_1_OR_GREATER +#if NETSTANDARD2_1_OR_GREATER // ReSharper disable once CheckNamespace namespace System.Runtime.CompilerServices; +using System.ComponentModel; + /// /// Reserved to be used by the compiler for tracking metadata. /// This class should not be used by developers in source code. diff --git a/Community.Wsl.Sdk/Strategies/Api/IEnvironment.cs b/Community.Wsl.Sdk/Strategies/Api/IEnvironment.cs index 3646c91..3c1fe39 100644 --- a/Community.Wsl.Sdk/Strategies/Api/IEnvironment.cs +++ b/Community.Wsl.Sdk/Strategies/Api/IEnvironment.cs @@ -1,8 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Text; - -namespace Community.Wsl.Sdk.Strategies.Api; +namespace Community.Wsl.Sdk.Strategies.Api; public interface IEnvironment { diff --git a/Community.Wsl.Sdk/Strategies/Api/IIo.cs b/Community.Wsl.Sdk/Strategies/Api/IIo.cs index 6dbf2b0..d881333 100644 --- a/Community.Wsl.Sdk/Strategies/Api/IIo.cs +++ b/Community.Wsl.Sdk/Strategies/Api/IIo.cs @@ -1,8 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Text; - -namespace Community.Wsl.Sdk.Strategies.Api; +namespace Community.Wsl.Sdk.Strategies.Api; public interface IIo { diff --git a/Community.Wsl.Sdk/Strategies/Commands/Command.cs b/Community.Wsl.Sdk/Strategies/Commands/Command.cs index 9f57e37..a0b249f 100644 --- a/Community.Wsl.Sdk/Strategies/Commands/Command.cs +++ b/Community.Wsl.Sdk/Strategies/Commands/Command.cs @@ -8,24 +8,23 @@ namespace Community.Wsl.Sdk.Strategies.Commands; public class Command : ICommand { - private ProcessStartInfo? _startInfo; - private IProcess? _process; - private bool _isStarted = false; - private bool _hasWaited = false; - private bool _isDisposed = false; - private readonly string _command; private readonly string[] _arguments; - private readonly CommandExecutionOptions _options; private readonly bool _asRoot; - private readonly bool _shellExecute; + private readonly string _command; private readonly string _distroName; - - private IStreamReader _stdoutReader; - private IStreamReader _stderrReader; + private readonly CommandExecutionOptions _options; + private readonly bool _shellExecute; private IEnvironment _environment; + private bool _hasWaited = false; private IIo _io; + private bool _isDisposed = false; + private bool _isStarted = false; + private IProcess? _process; private IProcessManager _processManager; + private ProcessStartInfo? _startInfo; + private IStreamReader _stderrReader; + private IStreamReader _stdoutReader; public Command( string distroName, @@ -52,9 +51,24 @@ public Command( _stderrReader = new StreamNullReader(); _stdoutReader = new StreamNullReader(); - _stderrReader = new StreamNullReader(); + + if ( + options.StdInDataProcessingMode + is DataProcessingMode.Binary + or DataProcessingMode.String + ) + { + throw new ArgumentException( + "StandardInput can only be dropped or external.", + nameof(options) + ); + } } + internal IStreamReader StderrReader => _stderrReader; + + internal IStreamReader StdoutReader => _stdoutReader; + public void Dispose() { if (!_isDisposed) @@ -77,7 +91,7 @@ public CommandStreams Start() { if (IsStarted) { - throw new ArgumentException("Command has already been started!"); + throw new Exception("Command has already been started!"); } bool redirectStandardInput = _options.StdInDataProcessingMode != DataProcessingMode.Drop; @@ -86,7 +100,11 @@ public CommandStreams Start() _isStarted = true; - CreateStartInfo(redirectStandardInput, redirectStandardOutput, redirectStandardError); + _startInfo = CreateStartInfo( + redirectStandardInput, + redirectStandardOutput, + redirectStandardError + ); var process = _processManager.Start(_startInfo) ?? throw new Exception("Cannot start wsl process."); @@ -115,102 +133,11 @@ public CommandStreams Start() }; } - private void CreateStartInfo(bool redirectStandardInput, bool redirectStandardOutput, bool redirectStandardError) - { - var wslPath = _io.Combine( - _environment.GetFolderPath(Environment.SpecialFolder.System), - "wsl.exe" - ); - - _startInfo = new ProcessStartInfo(wslPath); - - _startInfo.ArgumentList.Add("-d"); - _startInfo.ArgumentList.Add(_distroName); - - if (_asRoot) - { - if (_environment.OSVersion.Version.Build >= 22000) - { - // on Windows 11 use newer switch - _startInfo.ArgumentList.Add("--system"); - } - else - { - _startInfo.ArgumentList.Add("--user"); - _startInfo.ArgumentList.Add("root"); - } - } - - if (_shellExecute) - { - _startInfo.ArgumentList.Add("--"); - } - else - { - _startInfo.ArgumentList.Add("--exec"); - } - - _startInfo.ArgumentList.Add(_command); - foreach (string argument in _arguments) - { - _startInfo.ArgumentList.Add(argument); - } - - _startInfo.CreateNoWindow = true; - _startInfo.RedirectStandardInput = redirectStandardInput; - _startInfo.RedirectStandardOutput = redirectStandardOutput; - _startInfo.RedirectStandardError = redirectStandardError; - - if (redirectStandardOutput) - { - _startInfo.StandardOutputEncoding = _options.StdoutEncoding ?? Console.OutputEncoding; - } - - if (redirectStandardError) - { - _startInfo.StandardErrorEncoding = _options.StderrEncoding ?? Console.OutputEncoding; - } - - if (redirectStandardInput) - { - _startInfo.StandardInputEncoding = _options.StdinEncoding ?? Console.InputEncoding; - } - } - - private void CreateReader( - Func lazyStreamReader, - ref IStreamReader stdoutReader, - DataProcessingMode dataProcessingMode - ) - { - switch (dataProcessingMode) - { - case DataProcessingMode.Drop: - stdoutReader = new StreamNullReader(); - break; - case DataProcessingMode.Binary: - stdoutReader = new StreamDataReader(lazyStreamReader()); - break; - case DataProcessingMode.String: - stdoutReader = new StreamStringReader(lazyStreamReader()); - break; - case DataProcessingMode.External: - stdoutReader = new StreamNullReader(); - break; - default: - throw new ArgumentOutOfRangeException( - nameof(dataProcessingMode), - dataProcessingMode, - null - ); - } - } - public CommandResult WaitAndGetResults() { if (!IsStarted) { - throw new ArgumentException("Command hasn't been started, yet!"); + throw new Exception("Command hasn't been started, yet!"); } if (HasWaited) @@ -242,7 +169,7 @@ public async Task WaitAndGetResultsAsync() { if (!IsStarted) { - throw new ArgumentException("Command hasn't been started, yet!"); + throw new Exception("Command hasn't been started, yet!"); } if (HasWaited) @@ -282,6 +209,93 @@ public Task StartAndGetResultsAsync() return WaitAndGetResultsAsync(); } + private ProcessStartInfo CreateStartInfo( + bool redirectStandardInput, + bool redirectStandardOutput, + bool redirectStandardError + ) + { + var wslPath = _io.Combine( + _environment.GetFolderPath(Environment.SpecialFolder.System), + "wsl.exe" + ); + + var startInfo = new ProcessStartInfo(wslPath); + + AddArguments(startInfo); + + startInfo.CreateNoWindow = true; + startInfo.RedirectStandardInput = redirectStandardInput; + startInfo.RedirectStandardOutput = redirectStandardOutput; + startInfo.RedirectStandardError = redirectStandardError; + + if (redirectStandardOutput) + { + startInfo.StandardOutputEncoding = _options.StdoutEncoding ?? Console.OutputEncoding; + } + + if (redirectStandardError) + { + startInfo.StandardErrorEncoding = _options.StderrEncoding ?? Console.OutputEncoding; + } + + if (redirectStandardInput) + { + startInfo.StandardInputEncoding = _options.StdinEncoding ?? Console.InputEncoding; + } + + return startInfo; + } + + private void AddArguments(ProcessStartInfo startInfo) + { + startInfo.ArgumentList.Add("-d"); + startInfo.ArgumentList.Add(_distroName); + + if (_asRoot) + { + startInfo.ArgumentList.Add("--user"); + startInfo.ArgumentList.Add("root"); + } + + startInfo.ArgumentList.Add(_shellExecute ? "--" : "--exec"); + + startInfo.ArgumentList.Add(_command); + foreach (string argument in _arguments) + { + startInfo.ArgumentList.Add(argument); + } + } + + private void CreateReader( + Func lazyStreamReader, + ref IStreamReader stdoutReader, + DataProcessingMode dataProcessingMode + ) + { + switch (dataProcessingMode) + { + case DataProcessingMode.Drop: + stdoutReader = new StreamNullReader(); + break; + case DataProcessingMode.Binary: + stdoutReader = new StreamDataReader(lazyStreamReader()); + break; + case DataProcessingMode.String: + stdoutReader = new StreamStringReader(lazyStreamReader()); + break; + case DataProcessingMode.External: + stdoutReader = new StreamNullReader(); + break; + default: + throw new ArgumentOutOfRangeException( + nameof(dataProcessingMode), + dataProcessingMode, + null + ); + } + } + private Task WaitForExitAsync(IProcess process) { if (process.HasExited) @@ -301,7 +315,7 @@ private Task WaitForExitAsync(IProcess process) return tcs.Task; - void HasExited(object _, EventArgs __) + void HasExited(object? _, EventArgs __) { tcs.SetResult(process.ExitCode); diff --git a/Community.Wsl.Sdk/Strategies/Commands/NativePipe.cs b/Community.Wsl.Sdk/Strategies/Commands/NativePipe.cs deleted file mode 100644 index d4143d1..0000000 --- a/Community.Wsl.Sdk/Strategies/Commands/NativePipe.cs +++ /dev/null @@ -1,41 +0,0 @@ -using System; -using System.IO; -using Microsoft.Win32.SafeHandles; - -namespace Community.Wsl.Sdk.Strategies.Commands; - -public readonly struct NativePipe : IDisposable -{ - public readonly SafeFileHandle? ReadHandle { get; init; } - public readonly SafeFileHandle? WriteHandle { get; init; } - public readonly StreamReader Reader { get; init; } - public readonly StreamWriter Writer { get; init; } - - public NativePipe(SafeFileHandle? readHandle = null, SafeFileHandle? writeHandle = null) - { - ReadHandle = readHandle; - WriteHandle = writeHandle; - - Reader = StreamReader.Null; - Writer = StreamWriter.Null; - } - - public NativePipe((SafeFileHandle readHandle, SafeFileHandle writeHandle) tuple) - { - ReadHandle = tuple.readHandle; - WriteHandle = tuple.writeHandle; - - Reader = StreamReader.Null; - Writer = StreamWriter.Null; - } - - public void Dispose() - { - ReadHandle?.Dispose(); - WriteHandle?.Dispose(); - Reader?.Close(); - Reader?.Dispose(); - Writer?.Close(); - Writer?.Dispose(); - } -} diff --git a/Community.Wsl.Sdk/Strategies/Commands/StreamDataReader.cs b/Community.Wsl.Sdk/Strategies/Commands/StreamDataReader.cs index a33e43f..759e695 100644 --- a/Community.Wsl.Sdk/Strategies/Commands/StreamDataReader.cs +++ b/Community.Wsl.Sdk/Strategies/Commands/StreamDataReader.cs @@ -7,7 +7,7 @@ namespace Community.Wsl.Sdk.Strategies.Commands; internal class StreamDataReader : IStreamReader { - private StreamReader _reader; + protected readonly StreamReader _reader; private Thread? _thread; private byte[]? _data; private TaskCompletionSource? _completionSource; @@ -19,7 +19,7 @@ public StreamDataReader(StreamReader reader) public byte[]? Data => _data; - private void Finished(byte[] data) + protected virtual void Finished(byte[] data) { _data = data; _completionSource?.SetResult(data); @@ -29,7 +29,7 @@ public void Fetch() { if (_thread != null) { - throw new ArgumentException("Already started fetching!"); + throw new Exception("Already started fetching!"); } _completionSource = new TaskCompletionSource(); @@ -45,21 +45,21 @@ public void Fetch() var data = stream.ToArray(); Finished(data); } - ); + ) { IsBackground = true }; _thread.Start(); } - public void CopyResultTo(ref CommandResult result, bool isStdOut) + public virtual void CopyResultTo(ref CommandResult result, bool isStdOut) { if (_thread == null) { - throw new ArgumentException("Data hasn't been fetched, yet!"); + throw new Exception("Data hasn't been fetched, yet!"); } if (_thread.ThreadState != ThreadState.Stopped) { - throw new ArgumentException("Fetching hasn't been finished, yet!"); + throw new Exception("Fetching hasn't been finished, yet!"); } if (isStdOut) @@ -79,6 +79,11 @@ public void Wait() public Task WaitAsync() { - return _completionSource?.Task ?? Task.FromResult(Array.Empty()); + if (_completionSource == null) + { + return Task.CompletedTask; + } + + return _completionSource.Task.ContinueWith(task => Task.Delay(1), TaskScheduler.Default); } } diff --git a/Community.Wsl.Sdk/Strategies/Commands/StreamStringReader.cs b/Community.Wsl.Sdk/Strategies/Commands/StreamStringReader.cs index 4c3f129..b77fe7a 100644 --- a/Community.Wsl.Sdk/Strategies/Commands/StreamStringReader.cs +++ b/Community.Wsl.Sdk/Strategies/Commands/StreamStringReader.cs @@ -1,61 +1,24 @@ -using System; -using System.IO; -using System.Threading; -using System.Threading.Tasks; +using System.IO; namespace Community.Wsl.Sdk.Strategies.Commands; -internal class StreamStringReader : IStreamReader +internal class StreamStringReader : StreamDataReader { - private StreamReader _reader; - private Thread? _thread; private string? _data; - private TaskCompletionSource? _completionSource; - public StreamStringReader(StreamReader reader) - { - _reader = reader; - } + public StreamStringReader(StreamReader reader) : base(reader) { } - public string? Data => _data; + public new string? Data => _data; - private void Finished(string data) + protected override void Finished(byte[] data) { - _data = data; - _completionSource?.SetResult(data); + _data = _reader.CurrentEncoding.GetString(data); + base.Finished(data); } - public void Fetch() + public override void CopyResultTo(ref CommandResult result, bool isStdOut) { - if (_thread != null) - { - throw new ArgumentException("Already started fetching!"); - } - - _completionSource = new TaskCompletionSource(); - - _thread = new Thread( - () => - { - var content = _reader.ReadToEnd(); - Finished(content); - } - ); - - _thread.Start(); - } - - public void CopyResultTo(ref CommandResult result, bool isStdOut) - { - if (_thread == null) - { - throw new ArgumentException("Data hasn't been fetched, yet!"); - } - - if (_thread.ThreadState != ThreadState.Stopped) - { - throw new ArgumentException("Fetching hasn't been finished, yet!"); - } + base.CopyResultTo(ref result, isStdOut); if (isStdOut) { @@ -66,14 +29,4 @@ public void CopyResultTo(ref CommandResult result, bool isStdOut) result = result with { StderrData = null, Stderr = Data }; } } - - public void Wait() - { - _thread?.Join(); - } - - public Task WaitAsync() - { - return _completionSource?.Task ?? Task.FromResult(String.Empty); - } } diff --git a/generate-coverage-report.cmd b/generate-coverage-report.cmd new file mode 100644 index 0000000..86cc91c --- /dev/null +++ b/generate-coverage-report.cmd @@ -0,0 +1,5 @@ +generate-coverage.cmd + +rmdir /S /Q CoverageReport +reportgenerator "-reports:.\CoverageResults\coverage.net6.0.cobertura.xml" "-targetdir:CoverageReport" "-reporttypes:Html;HtmlSummary" +rmdir /S /Q CoverageResults diff --git a/generate-coverage.cmd b/generate-coverage.cmd new file mode 100644 index 0000000..21ebeb4 --- /dev/null +++ b/generate-coverage.cmd @@ -0,0 +1,2 @@ +dotnet test /p:TargetFramework=net6.0 /p:CollectCoverage=true /p:Exclude=\"[Community.Wsl.Sdk.Tests]*\" /p:CoverletOutput=../CoverageResults/ /p:MergeWith="../CoverageResults/coverage.net6.0.json" /p:CoverletOutputFormat=\"cobertura,json\" /p:ThresholdType=\"line,branch,method\" -m:1 + diff --git a/generate-coverage.sh b/generate-coverage.sh new file mode 100644 index 0000000..3d9e7c2 --- /dev/null +++ b/generate-coverage.sh @@ -0,0 +1,6 @@ +#!/usr/bin/env bash + +set -e + +dotnet test /p:TargetFramework=net6.0 /p:CollectCoverage=true /p:Exclude=\"[Community.Wsl.Sdk.Tests]*\" /p:CoverletOutput=../CoverageResults/ /p:MergeWith="../CoverageResults/coverage.net6.0.json" /p:CoverletOutputFormat=\"cobertura,json\" /p:ThresholdType=\"line,branch,method\" -m:1 +