Initial commit: Nix WSL Wrapper project with DDD, Clean Code, and tests

This commit is contained in:
Marek Novák
2026-07-01 02:04:43 +02:00
commit 161eb7d387
22 changed files with 932 additions and 0 deletions
+25
View File
@@ -0,0 +1,25 @@
## Build outputs
**/bin/
**/obj/
**/dist/
## User-specific files
*.user
*.userosscache
*.sln.docstates
## Visual Studio cache and settings
.vs/
.vs*
[Dd]ebug/
[Rr]elease/
## Test results
TestResults/
*.trx
## Rider / JetBrains settings
.idea/
## VS Code settings
.vscode/
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2026
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+10
View File
@@ -0,0 +1,10 @@
<Solution>
<Folder Name="/src/">
<Project Path="src/NixWslWrapper.Cli/NixWslWrapper.Cli.csproj" />
<Project Path="src/NixWslWrapper.Core/NixWslWrapper.Core.csproj" />
<Project Path="src/NixWslWrapper.Infrastructure/NixWslWrapper.Infrastructure.csproj" />
</Folder>
<Folder Name="/tests/">
<Project Path="tests/NixWslWrapper.Tests/NixWslWrapper.Tests.csproj" />
</Folder>
</Solution>
+62
View File
@@ -0,0 +1,62 @@
# Nix WSL Wrapper (nix.exe)
A robust, transparent Windows executable wrapper designed to intercept native Windows commands and execute them seamlessly inside Nix Subsystem for Linux (WSL). This wrapper is particularly useful for allowing Windows-based MCP servers (like `utensils/mcp-nixos`), IDE integrations, and build tools to run `nix` commands natively on a Windows host without requiring full native Nix compile chains.
## Key Features
* **Clean Architecture & SOLID**: Built in C# using Domain-Driven Design (DDD) to encapsulate domain validation (e.g. `WslTarget`, `NixArguments`).
* **Deadlock-Free I/O Streaming**: Spawns background threads to redirect standard input, standard output, and standard error without blocking host tools.
* **Premium CLI UI & Diagnostics**: Integrates `Spectre.Console` to render beautiful diagnostic dashboards, system statuses, and automated validation tests.
* **Highly Configuration-driven**: Environment variables allow configuring the WSL distribution name, username, and path to the Nix profile.
---
## Architectural Layout
* **NixWslWrapper.Core**: The domain layer defining value objects (`WslTarget`, `NixArguments`, `ExecutionResult`) and interfaces (`ICommandExecutor`, `IStreamCopier`).
* **NixWslWrapper.Infrastructure**: Contains execution strategies (`WslCommandExecutor`), builder patterns (`ProcessBuilder`), and threaded stream piping (`ThreadedStreamCopier`).
* **NixWslWrapper.Cli**: Presentation layer resolving services via Dependency Injection (`Microsoft.Extensions.DependencyInjection`), handling command forwarding, and presenting styled dashboard visuals with `Spectre.Console`.
* **NixWslWrapper.Tests**: The testing suite compiling xUnit tests and mock assertions.
---
## Getting Started
### Prerequisites
1. **WSL2** must be active.
2. A Linux distribution installed in WSL (e.g. `Debian` or `Ubuntu`).
3. **Nix Package Manager** installed inside the WSL distribution.
### Build and Publish
Compile the project to a single self-contained executable for Windows:
```powershell
dotnet publish src/NixWslWrapper.Cli/NixWslWrapper.Cli.csproj -c Release -r win-x64 --self-contained -p:PublishSingleFile=true -o ./dist
```
This will generate `nix.exe` inside the `./dist` folder. Place this executable in your Windows `%PATH%` (e.g., in a WinGet Links directory or your IDE binary directory).
---
## Configuration
Configure the wrapper using the following Windows environment variables:
| Environment Variable | Description | Default Value |
| :--- | :--- | :--- |
| `NIX_WSL_DISTRO` | Target WSL Linux distribution name. | `Debian` |
| `NIX_WSL_USER` | The user to execute WSL commands under. | `root` |
| `NIX_WSL_PROFILE` | Path to the nix-daemon profile script inside WSL. | `/nix/var/nix/profiles/default/etc/profile.d/nix-daemon.sh` |
---
## Command Flags
When running the wrapper, standard arguments are forwarded directly to WSL. Two special flags are intercepted by the wrapper for system administration:
* `--wsl-status`: Renders a premium diagnostic panel checking your WSL execution and verifying the Nix version inside your distro.
* `--wsl-help`: Displays wrapper-specific configuration details.
---
## Running Tests
Run the xUnit test suite from the root of the project:
```powershell
dotnet test
```
+193
View File
@@ -0,0 +1,193 @@
using System;
using System.Diagnostics;
using System.Linq;
using NixWslWrapper.Core.Domain;
using NixWslWrapper.Core.Interfaces;
using Spectre.Console;
namespace NixWslWrapper.Cli
{
public class CliController
{
private readonly ICommandExecutor _commandExecutor;
public CliController(ICommandExecutor commandExecutor)
{
_commandExecutor = commandExecutor ?? throw new ArgumentNullException(nameof(commandExecutor));
}
public int Run(string[] args)
{
// Intercept special configuration/status options
if (args.Contains("--wsl-status", StringComparer.OrdinalIgnoreCase))
{
RenderStatusDashboard();
return 0;
}
if (args.Contains("--wsl-help", StringComparer.OrdinalIgnoreCase))
{
RenderHelp();
return 0;
}
// Get configuration from environment variables or use defaults
string distro = Environment.GetEnvironmentVariable("NIX_WSL_DISTRO") ?? "Debian";
string user = Environment.GetEnvironmentVariable("NIX_WSL_USER") ?? "root";
string profile = Environment.GetEnvironmentVariable("NIX_WSL_PROFILE") ?? "/nix/var/nix/profiles/default/etc/profile.d/nix-daemon.sh";
WslTarget target;
try
{
target = new WslTarget(distro, user, profile);
}
catch (Exception ex)
{
AnsiConsole.MarkupLine($"[red]Configuration Error:[/] {ex.Message}");
return 1;
}
var nixArgs = new NixArguments(args);
var result = _commandExecutor.Execute(nixArgs, target);
if (!result.IsSuccess)
{
if (result.ErrorMessage != null)
{
AnsiConsole.MarkupLine($"[bold red]Execution Error:[/] {result.ErrorMessage}");
}
return result.ExitCode;
}
return result.ExitCode;
}
private static void RenderStatusDashboard()
{
AnsiConsole.Write(
new FigletText("Nix WSL Wrapper")
.Color(Color.DeepSkyBlue1));
var panel = new Panel(
new Markup(
"[bold]Nix WSL Wrapper Diagnostic Dashboard[/]\n" +
"Wraps native Windows MCP / CLI tool inputs and redirects them into Nix running inside WSL.\n\n" +
$"[grey]Generated at: {DateTime.Now:yyyy-MM-dd HH:mm:ss}[/]"
))
{
Border = BoxBorder.Rounded,
Padding = new Padding(1, 1, 1, 1),
Header = new PanelHeader("System Info")
};
AnsiConsole.Write(panel);
// Fetch Environment details
string distro = Environment.GetEnvironmentVariable("NIX_WSL_DISTRO") ?? "Debian (Default)";
string user = Environment.GetEnvironmentVariable("NIX_WSL_USER") ?? "root (Default)";
string profile = Environment.GetEnvironmentVariable("NIX_WSL_PROFILE") ?? "/nix/var/nix/profiles/default/etc/profile.d/nix-daemon.sh (Default)";
var table = new Table()
.Border(TableBorder.Rounded)
.BorderColor(Color.Grey)
.AddColumn("[bold]Setting[/]")
.AddColumn("[bold]Value[/]")
.AddColumn("[bold]Source[/]");
table.AddRow("WSL Target Distro", distro, Environment.GetEnvironmentVariable("NIX_WSL_DISTRO") != null ? "[green]Env Var[/]" : "[yellow]Fallback[/]");
table.AddRow("WSL Execution User", user, Environment.GetEnvironmentVariable("NIX_WSL_USER") != null ? "[green]Env Var[/]" : "[yellow]Fallback[/]");
table.AddRow("Nix Profile Path", profile, Environment.GetEnvironmentVariable("NIX_WSL_PROFILE") != null ? "[green]Env Var[/]" : "[yellow]Fallback[/]");
AnsiConsole.Write(new Rule("[yellow]Configured Parameters[/]") { Justification = Justify.Left });
AnsiConsole.Write(table);
// Run WSL check
AnsiConsole.Write(new Rule("[yellow]WSL Integration Status[/]") { Justification = Justify.Left });
AnsiConsole.Status()
.Spinner(Spinner.Known.Dots)
.Start("Verifying WSL and Nix installation...", ctx =>
{
bool wslOk = false;
string wslVersion = "Unknown";
try
{
var psi = new ProcessStartInfo("wsl.exe", "--status")
{
RedirectStandardOutput = true,
UseShellExecute = false,
CreateNoWindow = true
};
using var p = Process.Start(psi);
p?.WaitForExit(3000);
wslOk = p?.ExitCode == 0;
wslVersion = wslOk ? "WSL Active" : "WSL Error";
}
catch
{
wslVersion = "wsl.exe not found in PATH";
}
ctx.Status("Checking Nix inside WSL Distro...");
bool nixOk = false;
string nixVersion = "Not Found";
if (wslOk)
{
try
{
string targetDistro = Environment.GetEnvironmentVariable("NIX_WSL_DISTRO") ?? "Debian";
string targetUser = Environment.GetEnvironmentVariable("NIX_WSL_USER") ?? "root";
string targetProfile = Environment.GetEnvironmentVariable("NIX_WSL_PROFILE") ?? "/nix/var/nix/profiles/default/etc/profile.d/nix-daemon.sh";
var psi = new ProcessStartInfo("wsl.exe", $"-d {targetDistro} -u {targetUser} -- sh -c \". {targetProfile} && nix --version\"")
{
RedirectStandardOutput = true,
RedirectStandardError = true,
UseShellExecute = false,
CreateNoWindow = true
};
using var p = Process.Start(psi);
p?.WaitForExit(5000);
if (p?.ExitCode == 0)
{
nixOk = true;
nixVersion = p.StandardOutput.ReadToEnd().Trim();
}
else
{
nixVersion = "Nix execution failed inside WSL";
}
}
catch (Exception ex)
{
nixVersion = $"Error querying: {ex.Message}";
}
}
// Render Checks
var grid = new Grid().AddColumn().AddColumn();
grid.AddRow(new Markup("wsl.exe Executable Status:"), wslOk ? new Markup("[green]✔ OK[/]") : new Markup($"[red]✘ FAILED ({wslVersion})[/]"));
grid.AddRow(new Markup("Nix Engine in WSL Distro:"), nixOk ? new Markup($"[green]✔ OK ({nixVersion})[/]") : new Markup($"[red]✘ FAILED ({nixVersion})[/]"));
AnsiConsole.Write(new Panel(grid) { Border = BoxBorder.None });
});
}
private static void RenderHelp()
{
AnsiConsole.Write(
new FigletText("Nix WSL Wrapper")
.Color(Color.DeepSkyBlue1));
AnsiConsole.MarkupLine("[bold]Usage:[/] nix.exe [[nix-arguments-to-forward]]");
AnsiConsole.MarkupLine("[bold]Alternative flags (Wrapper specific):[/]");
AnsiConsole.MarkupLine(" [yellow]--wsl-status[/] Displays diagnostic status of WSL and Nix integration.");
AnsiConsole.MarkupLine(" [yellow]--wsl-help[/] Displays this help menu.");
AnsiConsole.WriteLine();
AnsiConsole.MarkupLine("[bold]Configuration Environment Variables:[/]");
AnsiConsole.MarkupLine(" [green]NIX_WSL_DISTRO[/] The target WSL distribution name (default: [grey]Debian[/]).");
AnsiConsole.MarkupLine(" [green]NIX_WSL_USER[/] The user to execute commands under (default: [grey]root[/]).");
AnsiConsole.MarkupLine(" [green]NIX_WSL_PROFILE[/] The path to the nix-daemon profile script (default: [grey]/nix/var/nix/profiles/default/etc/profile.d/nix-daemon.sh[/]).");
}
}
}
@@ -0,0 +1,19 @@
<Project Sdk="Microsoft.NET.Sdk">
<ItemGroup>
<ProjectReference Include="..\NixWslWrapper.Infrastructure\NixWslWrapper.Infrastructure.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="10.0.9" />
<PackageReference Include="Spectre.Console" Version="0.57.1" />
</ItemGroup>
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
</Project>
+27
View File
@@ -0,0 +1,27 @@
using System;
using Microsoft.Extensions.DependencyInjection;
using NixWslWrapper.Core.Interfaces;
using NixWslWrapper.Infrastructure.IO;
using NixWslWrapper.Infrastructure.Executors;
namespace NixWslWrapper.Cli
{
internal class Program
{
private static int Main(string[] args)
{
var serviceProvider = ConfigureServices();
var controller = serviceProvider.GetRequiredService<CliController>();
return controller.Run(args);
}
private static IServiceProvider ConfigureServices()
{
return new ServiceCollection()
.AddSingleton<IStreamCopier, ThreadedStreamCopier>()
.AddSingleton<ICommandExecutor, WslCommandExecutor>()
.AddSingleton<CliController>()
.BuildServiceProvider();
}
}
}
@@ -0,0 +1,36 @@
using System;
namespace NixWslWrapper.Core.Domain
{
public class ExecutionResult
{
public int ExitCode { get; }
public bool IsSuccess => ExitCode == 0;
public string? ErrorMessage { get; }
public ExecutionResult(int exitCode, string? errorMessage = null)
{
ExitCode = exitCode;
ErrorMessage = errorMessage;
}
public static ExecutionResult Success() => new ExecutionResult(0);
public static ExecutionResult Failure(int exitCode, string errorMessage) => new ExecutionResult(exitCode, errorMessage);
public override bool Equals(object? obj)
{
if (obj is not ExecutionResult other) return false;
return ExitCode == other.ExitCode && string.Equals(ErrorMessage, other.ErrorMessage, StringComparison.Ordinal);
}
public override int GetHashCode()
{
return HashCode.Combine(ExitCode, ErrorMessage);
}
public override string ToString()
{
return IsSuccess ? "Success" : $"Failed with Exit Code {ExitCode}. Error: {ErrorMessage}";
}
}
}
@@ -0,0 +1,55 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace NixWslWrapper.Core.Domain
{
public class NixArguments
{
public string[] RawArguments { get; }
public NixArguments(string[]? rawArguments)
{
RawArguments = rawArguments ?? Array.Empty<string>();
}
public string ToEscapedString()
{
if (RawArguments.Length == 0) return string.Empty;
var escapedList = new List<string>();
foreach (var arg in RawArguments)
{
// Escape existing double quotes
string escaped = arg.Replace("\"", "\\\"");
// Wrap in double quotes to prevent word splitting in bash
escapedList.Add($"\"{escaped}\"");
}
return string.Join(" ", escapedList);
}
public override bool Equals(object? obj)
{
if (obj is not NixArguments other) return false;
if (RawArguments.Length != other.RawArguments.Length) return false;
return RawArguments.SequenceEqual(other.RawArguments);
}
public override int GetHashCode()
{
int hash = 17;
foreach (var arg in RawArguments)
{
hash = hash * 23 + (arg?.GetHashCode() ?? 0);
}
return hash;
}
public override string ToString()
{
return string.Join(" ", RawArguments);
}
}
}
@@ -0,0 +1,52 @@
using System;
namespace NixWslWrapper.Core.Domain
{
public class WslTarget
{
public string Distribution { get; }
public string User { get; }
public string NixProfilePath { get; }
public WslTarget(string distribution, string user, string nixProfilePath = "/nix/var/nix/profiles/default/etc/profile.d/nix-daemon.sh")
{
if (string.IsNullOrWhiteSpace(distribution))
throw new ArgumentException("WSL Distribution name cannot be null or empty.", nameof(distribution));
if (string.IsNullOrWhiteSpace(user))
throw new ArgumentException("WSL User cannot be null or empty.", nameof(user));
if (string.IsNullOrWhiteSpace(nixProfilePath))
throw new ArgumentException("Nix Profile Path cannot be null or empty.", nameof(nixProfilePath));
Distribution = distribution.Trim();
User = user.Trim();
NixProfilePath = nixProfilePath.Trim();
}
public override bool Equals(object? obj)
{
if (obj is not WslTarget other) return false;
return string.Equals(Distribution, other.Distribution, StringComparison.OrdinalIgnoreCase) &&
string.Equals(User, other.User, StringComparison.OrdinalIgnoreCase) &&
string.Equals(NixProfilePath, other.NixProfilePath, StringComparison.Ordinal);
}
public override int GetHashCode()
{
return HashCode.Combine(
Distribution.ToLowerOrdinal(),
User.ToLowerOrdinal(),
NixProfilePath
);
}
public override string ToString()
{
return $"WSL Distro: {Distribution}, User: {User}, Profile: {NixProfilePath}";
}
}
internal static class StringExtensions
{
public static string ToLowerOrdinal(this string str) => str.ToLowerInvariant();
}
}
@@ -0,0 +1,9 @@
using NixWslWrapper.Core.Domain;
namespace NixWslWrapper.Core.Interfaces
{
public interface ICommandExecutor
{
ExecutionResult Execute(NixArguments arguments, WslTarget target);
}
}
@@ -0,0 +1,9 @@
using System.IO;
namespace NixWslWrapper.Core.Interfaces
{
public interface IStreamCopier
{
void Copy(Stream input, Stream output);
}
}
@@ -0,0 +1,9 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
</Project>
@@ -0,0 +1,61 @@
using System.Diagnostics;
namespace NixWslWrapper.Infrastructure.Executors
{
public class ProcessBuilder
{
private readonly ProcessStartInfo _startInfo = new();
public ProcessBuilder SetFileName(string fileName)
{
_startInfo.FileName = fileName;
return this;
}
public ProcessBuilder SetArguments(string arguments)
{
_startInfo.Arguments = arguments;
return this;
}
public ProcessBuilder RedirectInput(bool redirect = true)
{
_startInfo.RedirectStandardInput = redirect;
return this;
}
public ProcessBuilder RedirectOutput(bool redirect = true)
{
_startInfo.RedirectStandardOutput = redirect;
return this;
}
public ProcessBuilder RedirectError(bool redirect = true)
{
_startInfo.RedirectStandardError = redirect;
return this;
}
public ProcessBuilder UseShellExecute(bool useShell = false)
{
_startInfo.UseShellExecute = useShell;
return this;
}
public ProcessBuilder CreateNoWindow(bool noWindow = true)
{
_startInfo.CreateNoWindow = noWindow;
return this;
}
public ProcessStartInfo Build()
{
return _startInfo;
}
public Process CreateProcess()
{
return new Process { StartInfo = _startInfo };
}
}
}
@@ -0,0 +1,85 @@
using System;
using System.Diagnostics;
using System.IO;
using System.Threading;
using NixWslWrapper.Core.Domain;
using NixWslWrapper.Core.Interfaces;
namespace NixWslWrapper.Infrastructure.Executors
{
public class WslCommandExecutor : ICommandExecutor
{
private readonly IStreamCopier _streamCopier;
private readonly Stream _hostIn;
private readonly Stream _hostOut;
private readonly Stream _hostErr;
public WslCommandExecutor(
IStreamCopier streamCopier,
Stream? hostIn = null,
Stream? hostOut = null,
Stream? hostErr = null)
{
_streamCopier = streamCopier ?? throw new ArgumentNullException(nameof(streamCopier));
_hostIn = hostIn ?? Console.OpenStandardInput();
_hostOut = hostOut ?? Console.OpenStandardOutput();
_hostErr = hostErr ?? Console.OpenStandardError();
}
public ExecutionResult Execute(NixArguments arguments, WslTarget target)
{
if (arguments == null) throw new ArgumentNullException(nameof(arguments));
if (target == null) throw new ArgumentNullException(nameof(target));
string wslCmd = $". {target.NixProfilePath} && nix {arguments.ToEscapedString()}";
string wslArgs = $"-d {target.Distribution} -u {target.User} -- sh -c \"{wslCmd.Replace("\"", "\\\"")}\"";
var processBuilder = new ProcessBuilder()
.SetFileName("wsl.exe")
.SetArguments(wslArgs)
.RedirectInput(true)
.RedirectOutput(true)
.RedirectError(true)
.UseShellExecute(false)
.CreateNoWindow(true);
using (Process process = processBuilder.CreateProcess())
{
try
{
process.Start();
// Standard streams copy tasks
Thread tOut = new(() => _streamCopier.Copy(process.StandardOutput.BaseStream, _hostOut))
{
IsBackground = true
};
Thread tErr = new(() => _streamCopier.Copy(process.StandardError.BaseStream, _hostErr))
{
IsBackground = true
};
Thread tIn = new(() => _streamCopier.Copy(_hostIn, process.StandardInput.BaseStream))
{
IsBackground = true
};
tOut.Start();
tErr.Start();
tIn.Start();
process.WaitForExit();
// Wait up to 1 second for stream copier threads to finalize flushing buffer content
tOut.Join(1000);
tErr.Join(1000);
return new ExecutionResult(process.ExitCode);
}
catch (Exception ex)
{
return ExecutionResult.Failure(1, $"Error executing Nix wrapper: {ex.Message}");
}
}
}
}
}
@@ -0,0 +1,44 @@
using System;
using System.IO;
using NixWslWrapper.Core.Interfaces;
namespace NixWslWrapper.Infrastructure.IO
{
public class ThreadedStreamCopier : IStreamCopier
{
private readonly int _bufferSize;
public ThreadedStreamCopier(int bufferSize = 8192)
{
if (bufferSize <= 0)
throw new ArgumentOutOfRangeException(nameof(bufferSize), "Buffer size must be positive.");
_bufferSize = bufferSize;
}
public void Copy(Stream input, Stream output)
{
byte[] buffer = new byte[_bufferSize];
int read;
try
{
while ((read = input.Read(buffer, 0, buffer.Length)) > 0)
{
output.Write(buffer, 0, read);
output.Flush();
}
}
catch (IOException)
{
// Clean exit when stream is closed or process terminates
}
catch (ObjectDisposedException)
{
// Clean exit when streams are disposed
}
catch (Exception)
{
// General catch to ensure thread doesn't crash the host process
}
}
}
}
@@ -0,0 +1,13 @@
<Project Sdk="Microsoft.NET.Sdk">
<ItemGroup>
<ProjectReference Include="..\NixWslWrapper.Core\NixWslWrapper.Core.csproj" />
</ItemGroup>
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
</Project>
+46
View File
@@ -0,0 +1,46 @@
using System;
using Moq;
using NixWslWrapper.Cli;
using NixWslWrapper.Core.Domain;
using NixWslWrapper.Core.Interfaces;
using Xunit;
namespace NixWslWrapper.Tests
{
public class CliTests
{
[Fact]
public void CliController_ForwardArguments_ShouldReturnExecutorExitCode()
{
var mockExecutor = new Mock<ICommandExecutor>();
var expectedResult = new ExecutionResult(42);
mockExecutor
.Setup(m => m.Execute(It.IsAny<NixArguments>(), It.IsAny<WslTarget>()))
.Returns(expectedResult);
var controller = new CliController(mockExecutor.Object);
string[] cliArgs = new[] { "run", "nixpkgs#hello" };
int exitCode = controller.Run(cliArgs);
Assert.Equal(42, exitCode);
mockExecutor.Verify(m => m.Execute(
It.Is<NixArguments>(a => a.RawArguments.Length == 2 && a.RawArguments[0] == "run"),
It.IsAny<WslTarget>()
), Times.Once);
}
[Fact]
public void CliController_HelpMenuFlag_ShouldReturnZeroImmediately()
{
var mockExecutor = new Mock<ICommandExecutor>();
var controller = new CliController(mockExecutor.Object);
string[] cliArgs = new[] { "--wsl-help" };
int exitCode = controller.Run(cliArgs);
Assert.Equal(0, exitCode);
mockExecutor.Verify(m => m.Execute(It.IsAny<NixArguments>(), It.IsAny<WslTarget>()), Times.Never);
}
}
}
+71
View File
@@ -0,0 +1,71 @@
using System;
using NixWslWrapper.Core.Domain;
using Xunit;
namespace NixWslWrapper.Tests
{
public class CoreTests
{
[Fact]
public void WslTarget_ValidParameters_ShouldConstructCorrectly()
{
var target = new WslTarget("Debian", "root", "/some/path");
Assert.Equal("Debian", target.Distribution);
Assert.Equal("root", target.User);
Assert.Equal("/some/path", target.NixProfilePath);
}
[Theory]
[InlineData("", "root", "/path")]
[InlineData("Debian", "", "/path")]
[InlineData("Debian", "root", "")]
[InlineData(null, "root", "/path")]
[InlineData("Debian", null, "/path")]
[InlineData("Debian", "root", null)]
public void WslTarget_InvalidParameters_ShouldThrowArgumentException(string? distro, string? user, string? profile)
{
Assert.Throws<ArgumentException>(() => new WslTarget(distro!, user!, profile!));
}
[Fact]
public void WslTarget_Equality_ShouldBeTrueForSameValues()
{
var target1 = new WslTarget("Debian", "root", "/path");
var target2 = new WslTarget("debian", "ROOT", "/path"); // case-insensitive for distro/user
Assert.Equal(target1, target2);
}
[Fact]
public void NixArguments_EmptyArgs_ShouldReturnEmptyEscapedString()
{
var args = new NixArguments(Array.Empty<string>());
Assert.Equal(string.Empty, args.ToEscapedString());
}
[Fact]
public void NixArguments_WithSpecialCharacters_ShouldEscapeDoubleQuotes()
{
var args = new NixArguments(new[] { "run", "nixpkgs#hello", "--argstr", "msg", "hello \"world\"" });
string escaped = args.ToEscapedString();
Assert.Equal("\"run\" \"nixpkgs#hello\" \"--argstr\" \"msg\" \"hello \\\"world\\\"\"", escaped);
}
[Fact]
public void ExecutionResult_Success_ShouldSetCorrectProperties()
{
var result = ExecutionResult.Success();
Assert.True(result.IsSuccess);
Assert.Equal(0, result.ExitCode);
Assert.Null(result.ErrorMessage);
}
[Fact]
public void ExecutionResult_Failure_ShouldSetCorrectProperties()
{
var result = ExecutionResult.Failure(5, "Access Denied");
Assert.False(result.IsSuccess);
Assert.Equal(5, result.ExitCode);
Assert.Equal("Access Denied", result.ErrorMessage);
}
}
}
@@ -0,0 +1 @@
global using Xunit;
@@ -0,0 +1,52 @@
using System;
using System.IO;
using System.Text;
using NixWslWrapper.Infrastructure.IO;
using NixWslWrapper.Infrastructure.Executors;
using Xunit;
namespace NixWslWrapper.Tests
{
public class InfrastructureTests
{
[Fact]
public void ThreadedStreamCopier_ValidStream_ShouldCopyEntireContent()
{
var content = "Hello, Nix WSL Wrapper! Stream copying should work flawlessly.";
byte[] bytes = Encoding.UTF8.GetBytes(content);
using var input = new MemoryStream(bytes);
using var output = new MemoryStream();
var copier = new ThreadedStreamCopier(64); // small buffer to force multiple reads
copier.Copy(input, output);
byte[] resultBytes = output.ToArray();
string result = Encoding.UTF8.GetString(resultBytes);
Assert.Equal(content, result);
}
[Fact]
public void ProcessBuilder_ValidConfiguration_ShouldBuildCorrectProcessStartInfo()
{
var builder = new ProcessBuilder()
.SetFileName("my-app.exe")
.SetArguments("--arg1 val1")
.RedirectInput(true)
.RedirectOutput(true)
.RedirectError(true)
.UseShellExecute(false)
.CreateNoWindow(true);
var startInfo = builder.Build();
Assert.Equal("my-app.exe", startInfo.FileName);
Assert.Equal("--arg1 val1", startInfo.Arguments);
Assert.True(startInfo.RedirectStandardInput);
Assert.True(startInfo.RedirectStandardOutput);
Assert.True(startInfo.RedirectStandardError);
Assert.False(startInfo.UseShellExecute);
Assert.True(startInfo.CreateNoWindow);
}
}
}
@@ -0,0 +1,32 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>
<IsTestProject>true</IsTestProject>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.6.0" />
<PackageReference Include="Moq" Version="4.20.72" />
<PackageReference Include="xunit" Version="2.4.2" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.5">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="coverlet.collector" Version="6.0.0">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\NixWslWrapper.Core\NixWslWrapper.Core.csproj" />
<ProjectReference Include="..\..\src\NixWslWrapper.Infrastructure\NixWslWrapper.Infrastructure.csproj" />
<ProjectReference Include="..\..\src\NixWslWrapper.Cli\NixWslWrapper.Cli.csproj" />
</ItemGroup>
</Project>