-
Notifications
You must be signed in to change notification settings - Fork 0
Add cross-platform unit tests with MSTest framework #21
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from 6 commits
Commits
Show all changes
7 commits
Select commit
Hold shift + click to select a range
733a2dc
Initial plan
Copilot ff182d1
Add test project with cross-platform unit tests
Copilot ef0b2f8
Update build workflow to add cross-platform test job
Copilot d1797dd
Improve Runner class to capture stderr and properly dispose Process
Copilot c98c57d
Add explicit permissions to workflow jobs for security
Copilot 16fe7b7
Split tests into ProgramTests and IntegrationTests
Copilot 81f9cce
Use COMSPEC environment variable for Windows shell path
Copilot File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,3 +1,7 @@ | ||
| .vs | ||
| *.bak | ||
| node_modules | ||
| bin/ | ||
| obj/ | ||
| TestResults/ | ||
| drop/ |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
39 changes: 39 additions & 0 deletions
39
test/DemaConsulting.DotnetToolWrapper.Tests/DemaConsulting.DotnetToolWrapper.Tests.csproj
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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> |
253 changes: 253 additions & 0 deletions
253
test/DemaConsulting.DotnetToolWrapper.Tests/IntegrationTests.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,253 @@ | ||
| 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() | ||
| { | ||
| return RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? "cmd.exe" : "/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"); | ||
| } | ||
| } | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
"cmd.exe" is not a complete path to the windows shell. Consider replacing this with using environment variables or some other more reliable way to get an absolute path to the shell program.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@copilot Please try fixing this again - you encountered a failure last time because you got stuck in a loop forever failing to read CI workflow logs - do not try to read them when working on this.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed in commit 81f9cce. Changed
GetShellProgram()to use the COMSPEC environment variable on Windows, which provides the full path to cmd.exe (typicallyC:\Windows\System32\cmd.exe), instead of just "cmd.exe".