Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 38 additions & 0 deletions .github/workflows/build.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,46 @@ on:
type: string

jobs:
test:
name: Test (${{ matrix.os }})
runs-on: ${{ matrix.os }}
permissions:
contents: read
strategy:
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]
steps:

- uses: actions/checkout@v6

- name: Setup dotnet
uses: actions/setup-dotnet@v5
with:
dotnet-version: |
8.x
9.x
10.x

- name: Build
run: >
dotnet build
--configuration Release
--property:Version=${{ inputs.semver }}
--property:InformationalVersion=${{ inputs.semver }}
--property:AssemblyVersion=${{ inputs.winver }}

- name: Test
run: >
dotnet test
--configuration Release
--no-build

build:
name: Build Artifacts
needs: test
runs-on: ubuntu-latest
permissions:
contents: read
steps:

- uses: actions/checkout@v6
Expand Down
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
.vs
*.bak
node_modules
bin/
obj/
TestResults/
drop/
6 changes: 6 additions & 0 deletions DemaConsulting.DotnetToolWrapper.sln
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ VisualStudioVersion = 17.9.34622.214
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DemaConsulting.DotnetToolWrapper", "src\DemaConsulting.DotnetToolWrapper\DemaConsulting.DotnetToolWrapper.csproj", "{9381FBF3-DCAE-4D2E-9F20-341FDDD38537}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DemaConsulting.DotnetToolWrapper.Tests", "test\DemaConsulting.DotnetToolWrapper.Tests\DemaConsulting.DotnetToolWrapper.Tests.csproj", "{8E5F3A42-1D5B-4C9E-8F1D-2C3F4D5E6F7A}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand All @@ -15,6 +17,10 @@ Global
{9381FBF3-DCAE-4D2E-9F20-341FDDD38537}.Debug|Any CPU.Build.0 = Debug|Any CPU
{9381FBF3-DCAE-4D2E-9F20-341FDDD38537}.Release|Any CPU.ActiveCfg = Release|Any CPU
{9381FBF3-DCAE-4D2E-9F20-341FDDD38537}.Release|Any CPU.Build.0 = Release|Any CPU
{8E5F3A42-1D5B-4C9E-8F1D-2C3F4D5E6F7A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{8E5F3A42-1D5B-4C9E-8F1D-2C3F4D5E6F7A}.Debug|Any CPU.Build.0 = Debug|Any CPU
{8E5F3A42-1D5B-4C9E-8F1D-2C3F4D5E6F7A}.Release|Any CPU.ActiveCfg = Release|Any CPU
{8E5F3A42-1D5B-4C9E-8F1D-2C3F4D5E6F7A}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFrameworks>net8.0;net9.0;net10.0</TargetFrameworks>
<LangVersion>12</LangVersion>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>
<IsTestProject>true</IsTestProject>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<EnforceCodeStyleInBuild>true</EnforceCodeStyleInBuild>
<EnableNETAnalyzers>true</EnableNETAnalyzers>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="coverlet.collector" Version="6.0.2">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.CodeAnalysis.NetAnalyzers" Version="9.0.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
<PackageReference Include="MSTest.TestAdapter" Version="3.7.0" />
<PackageReference Include="MSTest.TestFramework" Version="3.7.0" />
<PackageReference Include="SonarAnalyzer.CSharp" Version="10.4.0.108396">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\..\src\DemaConsulting.DotnetToolWrapper\DemaConsulting.DotnetToolWrapper.csproj">
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
</ProjectReference>
</ItemGroup>

</Project>
260 changes: 260 additions & 0 deletions test/DemaConsulting.DotnetToolWrapper.Tests/IntegrationTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,260 @@
using System.Runtime.InteropServices;
using System.Text.Json;
using Microsoft.VisualStudio.TestTools.UnitTesting;

namespace DemaConsulting.DotnetToolWrapper.Tests;

/// <summary>
/// Integration tests for the DotnetToolWrapper application
/// </summary>
[TestClass]
public class IntegrationTests
{
/// <summary>
/// Test setup directory
/// </summary>
private string _testDirectory = string.Empty;

/// <summary>
/// Initialize test
/// </summary>
[TestInitialize]
public void TestInitialize()
{
// Create a unique test directory
_testDirectory = Path.Combine(Path.GetTempPath(), $"DotnetToolWrapperTests_{Guid.NewGuid()}");
Directory.CreateDirectory(_testDirectory);
}

/// <summary>
/// Cleanup test
/// </summary>
[TestCleanup]
public void TestCleanup()
{
// Clean up test directory
if (Directory.Exists(_testDirectory))
{
Directory.Delete(_testDirectory, true);
}

// Clean up config file next to DLL
var dllPath = GetDotnetToolWrapperDllPath();
var dllDirectory = Path.GetDirectoryName(dllPath);
if (dllDirectory != null)
{
var configPath = Path.Combine(dllDirectory, "DotnetToolWrapper.json");
if (File.Exists(configPath))
{
File.Delete(configPath);
}
}
}

/// <summary>
/// Get the path to the DotnetToolWrapper DLL
/// </summary>
/// <returns>Path to DLL</returns>
private static string GetDotnetToolWrapperDllPath()
{
var assemblyLocation = Path.GetDirectoryName(typeof(IntegrationTests).Assembly.Location);
Assert.IsNotNull(assemblyLocation, "Assembly location should not be null");
return Path.Combine(assemblyLocation, "DemaConsulting.DotnetToolWrapper.dll");
}

/// <summary>
/// Create a DotnetToolWrapper.json configuration file
/// </summary>
/// <param name="program">Program to execute</param>
/// <param name="directory">Directory for config file (defaults to DLL directory)</param>
private static void CreateConfigFile(string program, string? directory = null)
{
// Default to DLL directory
if (directory == null)
{
var dllPath = GetDotnetToolWrapperDllPath();
directory = Path.GetDirectoryName(dllPath);
Assert.IsNotNull(directory, "DLL directory should not be null");
}

var target = Program.GetTarget();

var json = JsonSerializer.Serialize(new Dictionary<string, object>
{
{ target, new { program } }
});

File.WriteAllText(Path.Combine(directory, "DotnetToolWrapper.json"), json);
}

/// <summary>
/// Get the shell program name for the current OS
/// </summary>
/// <returns>Shell program name</returns>
private static string GetShellProgram()
{
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
// Use COMSPEC environment variable to get full path to cmd.exe
var comspec = Environment.GetEnvironmentVariable("COMSPEC");
return comspec ?? throw new InvalidOperationException("COMSPEC environment variable not found");
}

return "/bin/sh";
}

/// <summary>
/// Get shell arguments for exit code test
/// </summary>
/// <param name="exitCode">Exit code to test</param>
/// <returns>Shell arguments</returns>
private static string[] GetExitCodeArgs(int exitCode)
{
return RuntimeInformation.IsOSPlatform(OSPlatform.Windows)
? ["/c", $"exit {exitCode}"]
: ["-c", $"exit {exitCode}"];
}

/// <summary>
/// Get shell arguments for echo test
/// </summary>
/// <param name="text">Text to echo</param>
/// <returns>Shell arguments</returns>
private static string[] GetEchoArgs(string text)
{
return RuntimeInformation.IsOSPlatform(OSPlatform.Windows)
? ["/c", $"echo {text}"]
: ["-c", $"echo {text}"];
}

/// <summary>
/// Test that missing configuration file results in expected error
/// </summary>
[TestMethod]
public void TestMissingConfigFile()
{
// Arrange
var dllPath = GetDotnetToolWrapperDllPath();

// Act
var exitCode = Runner.Run(out var output, "dotnet", dllPath);

// Assert
Assert.AreEqual(1, exitCode, "Exit code should be 1 for missing config file");
Assert.IsTrue(output.Contains("Missing configuration file"), "Output should mention missing config file");
Assert.IsTrue(output.Contains("DotnetToolWrapper.json"), "Output should mention config file name");
}

/// <summary>
/// Test that exit codes are properly passed through
/// </summary>
[TestMethod]
[DataRow(0)]
[DataRow(1)]
[DataRow(42)]
[DataRow(255)]
public void TestExitCodes(int expectedExitCode)
{
// Arrange
var dllPath = GetDotnetToolWrapperDllPath();
var shellProgram = GetShellProgram();

// Create config file pointing to shell
CreateConfigFile(shellProgram);

// Get shell arguments to exit with specific code
var shellArgs = GetExitCodeArgs(expectedExitCode);

// Prepare arguments for dotnet command
var args = new List<string> { dllPath };
args.AddRange(shellArgs);

// Act
var exitCode = Runner.Run(out _, "dotnet", args.ToArray());

// Assert
Assert.AreEqual(expectedExitCode, exitCode, $"Exit code should be {expectedExitCode}");
}

/// <summary>
/// Test that arguments are properly passed through
/// </summary>
[TestMethod]
public void TestArgumentPassing()
{
// Arrange
var dllPath = GetDotnetToolWrapperDllPath();
var shellProgram = GetShellProgram();
var testText = "HelloWorld";

// Create config file pointing to shell
CreateConfigFile(shellProgram);

// Get shell arguments to echo text
var shellArgs = GetEchoArgs(testText);

// Prepare arguments for dotnet command
var args = new List<string> { dllPath };
args.AddRange(shellArgs);

// Act
var exitCode = Runner.Run(out var output, "dotnet", args.ToArray());

// Assert
Assert.AreEqual(0, exitCode, "Exit code should be 0");
Assert.IsTrue(output.Contains(testText), $"Output should contain '{testText}'");
}

/// <summary>
/// Test that unsupported target results in expected error
/// </summary>
[TestMethod]
public void TestUnsupportedTarget()
{
// Arrange
var dllPath = GetDotnetToolWrapperDllPath();
var dllDirectory = Path.GetDirectoryName(dllPath);
Assert.IsNotNull(dllDirectory, "DLL directory should not be null");

// Create config file with fake target
var json = JsonSerializer.Serialize(new Dictionary<string, object>
{
{ "fake-target", new { program = "fake" } }
});
File.WriteAllText(Path.Combine(dllDirectory, "DotnetToolWrapper.json"), json);

// Act
var exitCode = Runner.Run(out var output, "dotnet", dllPath);

// Assert
Assert.AreEqual(1, exitCode, "Exit code should be 1 for unsupported target");
Assert.IsTrue(output.Contains("does not support"), "Output should mention unsupported target");
}

/// <summary>
/// Test that bad configuration results in expected error
/// </summary>
[TestMethod]
public void TestBadConfiguration()
{
// Arrange
var dllPath = GetDotnetToolWrapperDllPath();
var dllDirectory = Path.GetDirectoryName(dllPath);
Assert.IsNotNull(dllDirectory, "DLL directory should not be null");
var target = Program.GetTarget();

// Create config file without program property
var json = JsonSerializer.Serialize(new Dictionary<string, object>
{
{ target, new { notprogram = "fake" } }
});
File.WriteAllText(Path.Combine(dllDirectory, "DotnetToolWrapper.json"), json);

// Act
var exitCode = Runner.Run(out var output, "dotnet", dllPath);

// Assert
Assert.AreEqual(1, exitCode, "Exit code should be 1 for bad configuration");
Assert.IsTrue(output.Contains("Bad configuration"), "Output should mention bad configuration");
}
}
Loading