Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
78 changes: 41 additions & 37 deletions .github/copilot-instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -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: `<type>(<scope>): <subject>`
- Types: `feat`, `fix`, `docs`, `style`, `refactor`, `perf`, `test`, `build`, `ci`, `chore`, `revert`
Expand All @@ -38,48 +65,25 @@ 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`.
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 <path-to-stryker.dll>` 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`.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should work with the Stryker on Stryker script


## Adding a mutator

Comment on lines +85 to 86
## 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<T>` 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<T>` 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
When adding a mutator see the full guide in [adding_a_mutator.md](../adding_a_mutator.md).
Original file line number Diff line number Diff line change
@@ -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);

Comment on lines +48 to +53
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<TestNode>(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<TestNode>(json);

testNode.ShouldNotBeNull();
testNode.Uid.ShouldBe("uid-1");
testNode.LocationFile.ShouldBeNull();
testNode.LocationLineStart.ShouldBeNull();
testNode.LocationType.ShouldBeNull();
testNode.LocationMethod.ShouldBeNull();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,19 +10,32 @@ public sealed class MtpTestCase : ITestCase
public MtpTestCase(TestNode testNode)
{
_testNode = testNode;
CodeFilePath = testNode.LocationFile ?? string.Empty;
LineNumber = testNode.LocationLineStart ?? 0;
Comment on lines +13 to +14
FullyQualifiedName = BuildFullyQualifiedName(testNode);
Comment on lines 12 to +15
}

public string FullyQualifiedName { get; }
public Uri Uri => new("executor://MicrosoftTestPlatform");
public int LineNumber { get; }

public string Source { get; }
public string CodeFilePath => string.Empty;
public string CodeFilePath { get; }

public string AssemblyPath { get; init; }

public Guid Guid { get; }
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;
}
}
17 changes: 16 additions & 1 deletion src/Stryker.TestRunner.MicrosoftTestPlatform/Models/TestNode.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Loading