diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index b96a6e6dc8..bec2501fd1 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -16,16 +16,43 @@ Reference these instruction files when applicable: ## Project Overview -Stryker.NET is a mutation testing framework for .NET projects. It allows you to test your tests by temporarily inserting bugs (mutations) in your source code to verify that tests catch them. +Stryker.NET is a mutation testing framework for .NET projects. It allows you to test your tests by temporarily inserting bugs (mutations) in your source code to verify that tests catch them. Some of the key features of Stryker.NET include: + +- Large set of built-in mutators for C# code +- Support for multiple test frameworks (xUnit, NUnit, MSTest, TUnit) +- Integration with Visual Studio Test platform +- Integration with the Microsoft Testing Platform (MTP) +- Detailed reporting of mutation testing results using [html and json report formats](https://github.com/stryker-mutator/mutation-testing-elements) +- Support for .NET Core and .NET Framework projects +- Reporting to the [Stryker Dashboard](https://dashboard.stryker-mutator.io/) +- Configurable mutation testing options and thresholds + +Directory structure: +- `/src`: Main source code for Stryker.NET + - `/src/Stryker.CLI`: Command-line interface for running Stryker.NET + - `/src/Stryker.Core`: Core mutation testing engine. Contains the logic for analyzing projects, generating mutants, and reporting results. + - `/src/Stryker.TestRunner`: Test runner integration for executing tests during mutation testing + - `/src/Stryker.TestRunner.VsTest`: Test runner using the VsTest adapter for running tests with Visual Studio Test framework + - `/src/Stryker.TestRunner.MicrosoftTestPlatform`: Test runner for Microsoft Testing Platform (MTP) + - `/src/Stryker.Configuration`: Configuration and options management for Stryker.NET + - `/src/Stryker.Abstractions`: Common interfaces and abstractions used across the project + - `/src/Stryker.Utilities`: Utility functions and shared code used across the project +- `/docs`: Documentation for Stryker.NET +- `/integrationtest`: Integration tests for Stryker.NET + - `/integrationtest/TargetProjects`: Target projects used for testing major features of stryker. For example different runtimes, test frameworks, and project types. These projects are used for testing stryker's core features. + - `/integrationtest/Validation`: The tests validating the results of running stryker on the target projects +- `/ExampleProjects`: Example projects used only for testing F# ## Contributing Workflow ### Code Standards + - Follow the repository's `.editorconfig` and [Microsoft C# coding guidelines](https://docs.microsoft.com/en-us/dotnet/csharp/programming-guide/inside-a-program/coding-conventions) - Create or edit unit tests or integration tests for all changes - Update documentation when adding features ### Pull Request Title Convention + When creating or updating pull requests, **always** use Angular-style conventional commit format for PR titles: - Format: `(): ` - Types: `feat`, `fix`, `docs`, `style`, `refactor`, `perf`, `test`, `build`, `ci`, `chore`, `revert` @@ -38,48 +65,23 @@ When creating or updating pull requests, **always** use Angular-style convention **Why**: The project uses squash merging, so the PR title becomes the commit message in the main branch history. ### Running Tests + - **Unit tests**: Run `dotnet test` in the `/src` directory -- **Integration tests**: +- **Integration tests**: - On **Windows**: Run `.\integration-tests.ps1` in the root of the repo (PowerShell 7 recommended) - On **macOS/Linux**: Run `pwsh ./integration-tests.ps1` in the root of the repo (requires [PowerShell 7](https://learn.microsoft.com/en-us/powershell/scripting/install/installing-powershell)) -- Always run unit tests and integration tests before committing changes +- Always run unit tests and integration tests after making a change ### Testing Locally -To test Stryker.NET on a project: -1. In `Stryker.CLI`, open `properties > Debug` -2. Create a new Debug profile -3. Set `Launch` as `Project` -4. Set `WorkingDirectory` to a unit test project directory -5. You can use projects in `.\integrationtest\TargetProjects` for testing -6. Run with `Stryker.CLI` as the startup project - -**Note**: Running Stryker on itself doesn't work as assemblies will be in use. To run Stryker on the stryker codebase, use the official nuget release via `dotnet tool install dotnet-stryker` and then `dotnet stryker`. - -## Adding a New Mutator - -See the full guide in [adding_a_mutator.md](../adding_a_mutator.md). - -### Key Points for Mutators -1. **Purpose**: Generate mutations that look like possible human errors, not just any mutation -2. **Performance**: Keep mutators fast - they're called on every syntax element -3. **Buildable**: Generated mutations should compile in most situations -4. **Killable**: Avoid mutations that always raise exceptions or are semantically equivalent -5. **General**: Mutators should work for all projects, not be framework-specific - -### Implementation Steps -1. Create a class inheriting from `MutatorBase` and implementing `IMutator` -2. Specify the expected `SyntaxNode` class you can mutate (e.g., `StatementSyntax`) -3. Override the `MutationLevel` property (typically `Complete` or `Advanced`) -4. Override `ApplyMutation` to generate mutations -5. Add an entry in the `Mutator` enum -6. Create an instance in the `CsharpMutantOrchestrator` constructor -7. Add comprehensive unit tests -8. Update [docs/mutations.md](../docs/mutations.md) - -### Mutator Guidelines -- Use Roslyn APIs to work with syntax trees, not text transformations -- Each mutator is called on every syntax element recursively -- Return an empty list or `yield break` if no mutation can be generated -- Mutators must not throw exceptions -- Support various C# syntax versions (expression body vs block statement) -- Invest in unit tests early - look at existing mutator tests for examples + +To test Stryker.NET on a project in a terminal, you can build Stryker.NET and then run the resulting `stryker.dll` on the target project. Run `dotnet ` in the root of the project you want to test (adjust path as needed based on your build configuration). + +For example in the `/integrationtest/TargetProjects/NetCore/TargetProject` directory, you can run `dotnet ../../src/Stryker.CLI/bin/Debug/net8.0/stryker.dll` to test Stryker.NET on the target projects. + +### Running Stryker on itself + +Running Stryker on itself doesn't work as assemblies will be in use. To run Stryker on the stryker codebase, use the official nuget release via `dotnet tool install dotnet-stryker` and then `dotnet stryker`. + +## Adding a mutator + +When adding a mutator see the full guide in [adding_a_mutator.md](../adding_a_mutator.md). diff --git a/src/Stryker.TestRunner.MicrosoftTestPlatform.UnitTest/MtpTestCaseTests.cs b/src/Stryker.TestRunner.MicrosoftTestPlatform.UnitTest/MtpTestCaseTests.cs new file mode 100644 index 0000000000..980e7f3fe0 --- /dev/null +++ b/src/Stryker.TestRunner.MicrosoftTestPlatform.UnitTest/MtpTestCaseTests.cs @@ -0,0 +1,121 @@ +using System.Text.Json; +using Shouldly; +using Stryker.TestRunner.MicrosoftTestPlatform.Models; + +namespace Stryker.TestRunner.MicrosoftTestPlatform.UnitTest; + +[TestClass] +public class MtpTestCaseTests +{ + [TestMethod] + public void MtpTestCase_WithLocationProperties_PopulatesCodeFilePath() + { + var testNode = new TestNode("uid-1", "TestMethod1", "action", "discovered", + LocationFile: "/path/to/TestFile.cs", + LocationLineStart: 42); + + var testCase = new MtpTestCase(testNode); + + testCase.CodeFilePath.ShouldBe("/path/to/TestFile.cs"); + testCase.LineNumber.ShouldBe(42); + } + + [TestMethod] + public void MtpTestCase_WithLocationTypeAndMethod_BuildsFullyQualifiedName() + { + var testNode = new TestNode("uid-1", "TestMethod1", "action", "discovered", + LocationType: "MyNamespace.MyTestClass", + LocationMethod: "TestMethod1"); + + var testCase = new MtpTestCase(testNode); + + testCase.FullyQualifiedName.ShouldBe("MyNamespace.MyTestClass.TestMethod1"); + } + + [TestMethod] + public void MtpTestCase_WithoutLocationProperties_UsesDefaults() + { + var testNode = new TestNode("uid-1", "TestMethod1", "action", "discovered"); + + var testCase = new MtpTestCase(testNode); + + testCase.CodeFilePath.ShouldBe(string.Empty); + testCase.LineNumber.ShouldBe(0); + testCase.FullyQualifiedName.ShouldBe("uid-1"); + } + + [TestMethod] + public void MtpTestCase_WithoutLocationType_FallsBackToUid() + { + var testNode = new TestNode("uid-1", "TestMethod1", "action", "discovered", + LocationFile: "/path/to/TestFile.cs", + LocationLineStart: 10); + + var testCase = new MtpTestCase(testNode); + + testCase.FullyQualifiedName.ShouldBe("uid-1"); + } + + [TestMethod] + public void MtpTestCase_PreservesBasicProperties() + { + var testNode = new TestNode("uid-1", "TestMethod1", "action", "discovered"); + + var testCase = new MtpTestCase(testNode); + + testCase.Id.ShouldBe("uid-1"); + testCase.Name.ShouldBe("TestMethod1"); + testCase.Uri.ShouldBe(new Uri("executor://MicrosoftTestPlatform")); + } + + [TestMethod] + public void TestNode_DeserializesLocationPropertiesFromJson() + { + var json = """ + { + "uid": "uid-1", + "display-name": "TestMethod1", + "node-type": "action", + "execution-state": "discovered", + "location.file": "/path/to/TestFile.cs", + "location.line-start": 42, + "location.line-end": 50, + "location.type": "MyNamespace.MyTestClass", + "location.method": "TestMethod1" + } + """; + + var testNode = JsonSerializer.Deserialize(json); + + testNode.ShouldNotBeNull(); + testNode.Uid.ShouldBe("uid-1"); + testNode.DisplayName.ShouldBe("TestMethod1"); + testNode.LocationFile.ShouldBe("/path/to/TestFile.cs"); + testNode.LocationLineStart.ShouldBe(42); + testNode.LocationLineEnd.ShouldBe(50); + testNode.LocationType.ShouldBe("MyNamespace.MyTestClass"); + testNode.LocationMethod.ShouldBe("TestMethod1"); + } + + [TestMethod] + public void TestNode_DeserializesWithoutLocationPropertiesFromJson() + { + var json = """ + { + "uid": "uid-1", + "display-name": "TestMethod1", + "node-type": "action", + "execution-state": "discovered" + } + """; + + var testNode = JsonSerializer.Deserialize(json); + + testNode.ShouldNotBeNull(); + testNode.Uid.ShouldBe("uid-1"); + testNode.LocationFile.ShouldBeNull(); + testNode.LocationLineStart.ShouldBeNull(); + testNode.LocationType.ShouldBeNull(); + testNode.LocationMethod.ShouldBeNull(); + } +} diff --git a/src/Stryker.TestRunner.MicrosoftTestPlatform/Models/MtpTestCase.cs b/src/Stryker.TestRunner.MicrosoftTestPlatform/Models/MtpTestCase.cs index b76cb63ace..6279796681 100644 --- a/src/Stryker.TestRunner.MicrosoftTestPlatform/Models/MtpTestCase.cs +++ b/src/Stryker.TestRunner.MicrosoftTestPlatform/Models/MtpTestCase.cs @@ -10,6 +10,9 @@ public sealed class MtpTestCase : ITestCase public MtpTestCase(TestNode testNode) { _testNode = testNode; + CodeFilePath = testNode.LocationFile ?? string.Empty; + LineNumber = testNode.LocationLineStart ?? 0; + FullyQualifiedName = BuildFullyQualifiedName(testNode); } public string FullyQualifiedName { get; } @@ -17,7 +20,7 @@ public MtpTestCase(TestNode testNode) public int LineNumber { get; } public string Source { get; } - public string CodeFilePath => string.Empty; + public string CodeFilePath { get; } public string AssemblyPath { get; init; } @@ -25,4 +28,14 @@ public MtpTestCase(TestNode testNode) public string Name => _testNode.DisplayName; public string Id => _testNode.Uid; + + private static string BuildFullyQualifiedName(TestNode testNode) + { + if (testNode.LocationType is not null && testNode.LocationMethod is not null) + { + return $"{testNode.LocationType}.{testNode.LocationMethod}"; + } + + return testNode.Uid; + } } diff --git a/src/Stryker.TestRunner.MicrosoftTestPlatform/Models/TestNode.cs b/src/Stryker.TestRunner.MicrosoftTestPlatform/Models/TestNode.cs index 43feb00c94..28b2170ca6 100644 --- a/src/Stryker.TestRunner.MicrosoftTestPlatform/Models/TestNode.cs +++ b/src/Stryker.TestRunner.MicrosoftTestPlatform/Models/TestNode.cs @@ -16,4 +16,19 @@ public sealed record TestNode string NodeType, [property: JsonPropertyName("execution-state")] - string ExecutionState); + string ExecutionState, + + [property: JsonPropertyName("location.file")] + string? LocationFile = null, + + [property: JsonPropertyName("location.line-start")] + int? LocationLineStart = null, + + [property: JsonPropertyName("location.line-end")] + int? LocationLineEnd = null, + + [property: JsonPropertyName("location.type")] + string? LocationType = null, + + [property: JsonPropertyName("location.method")] + string? LocationMethod = null); diff --git a/src/Stryker.TestRunner/Results/TestRunResult.cs b/src/Stryker.TestRunner/Results/TestRunResult.cs index c155c2c38a..ae513040aa 100644 --- a/src/Stryker.TestRunner/Results/TestRunResult.cs +++ b/src/Stryker.TestRunner/Results/TestRunResult.cs @@ -28,7 +28,9 @@ public TestRunResult( IEnumerable messages, TimeSpan timeSpan) { - TestDescriptions = vsTestDescriptions.Where(p => executedTests.GetIdentifiers().Contains(p.Id)).ToList(); + TestDescriptions = executedTests.IsEveryTest + ? vsTestDescriptions.ToList() + : vsTestDescriptions.Where(p => executedTests.GetIdentifiers().Contains(p.Id)).ToList(); ExecutedTests = executedTests; FailingTests = failedTests; TimedOutTests = timedOutTest;