Compare commits
10 commits
6728ffeee8
...
bd7f0e70e1
| Author | SHA1 | Date | |
|---|---|---|---|
| bd7f0e70e1 | |||
| c93f87d282 | |||
| c6996d5bbf | |||
| 2dc2c288fb | |||
| 10ddbef963 | |||
| 05deb1f089 | |||
| b429296967 | |||
| c2e5a01e26 | |||
| 442fe5d5d8 | |||
| ccb456d5b0 |
6 changed files with 255 additions and 66 deletions
14
.editorconfig
Normal file
14
.editorconfig
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
root = true
|
||||||
|
|
||||||
|
[*]
|
||||||
|
indent_style = space
|
||||||
|
charset = utf-8
|
||||||
|
end_of_line = lf
|
||||||
|
insert_final_newline = true
|
||||||
|
trim_trailing_whitespace = true
|
||||||
|
|
||||||
|
[*.nix]
|
||||||
|
indent_size = 2
|
||||||
|
|
||||||
|
[*.{fs,fsi,fsx}]
|
||||||
|
indent_size = 4
|
||||||
|
|
@ -6,17 +6,22 @@ open Xunit
|
||||||
|
|
||||||
type CalculatorTests() =
|
type CalculatorTests() =
|
||||||
[<Fact>]
|
[<Fact>]
|
||||||
[<MutationCase("calc-add-one", "Example.Tests/Calculator.fs", 4, "value + 1", "value - 1")>]
|
[<MutationCase("""
|
||||||
member _.AddOne_increments() =
|
diff --git a/Example.Tests/Calculator.fs b/Example.Tests/Calculator.fs
|
||||||
Assert.Equal(42, Calculator.addOne 41)
|
index cfcce3b..39be7f3 100644
|
||||||
|
--- a/Example.Tests/Calculator.fs
|
||||||
|
+++ b/Example.Tests/Calculator.fs
|
||||||
|
@@ -1,7 +1,7 @@
|
||||||
|
namespace Example
|
||||||
|
|
||||||
[<Fact>]
|
module Calculator =
|
||||||
[<MutationCase("calc-abs-diff-branch", "Example.Tests/Calculator.fs", 7, "left - right", "right - left")>]
|
- let addOne value = value + 1
|
||||||
member _.AbsoluteDifference_preserves_order() =
|
+ let addOne value = value - 1
|
||||||
Assert.Equal(7, Calculator.absoluteDifference 10 3)
|
|
||||||
|
|
||||||
[<Fact>]
|
let absoluteDifference left right =
|
||||||
[<MutationCase("calc-leap-year-century", "Example.Tests/Calculator.fs", 10, "year % 100 <> 0", "year % 100 = 0")>]
|
if left >= right then left - right else right - left
|
||||||
member _.LeapYear_handles_centuries() =
|
|
||||||
Assert.True(Calculator.isLeapYear 2000)
|
member _.AddOne_increments() =
|
||||||
Assert.False(Calculator.isLeapYear 1900)
|
Assert.Equal(42, Calculator.addOne 41)
|
||||||
|
""")>]
|
||||||
|
member _.AddOne_increments() = Assert.Equal(42, Calculator.addOne 41)
|
||||||
|
|
|
||||||
|
|
@ -3,11 +3,7 @@ namespace Mutannot
|
||||||
open System
|
open System
|
||||||
|
|
||||||
[<AttributeUsage(AttributeTargets.Method, AllowMultiple = true)>]
|
[<AttributeUsage(AttributeTargets.Method, AllowMultiple = true)>]
|
||||||
type MutationCaseAttribute(id: string, file: string, line: int, find: string, replace: string) =
|
type MutationCaseAttribute(patch: string) =
|
||||||
inherit Attribute()
|
inherit Attribute()
|
||||||
|
|
||||||
member _.Id = id
|
member _.Patch = patch
|
||||||
member _.File = file
|
|
||||||
member _.Line = line
|
|
||||||
member _.Find = find
|
|
||||||
member _.Replace = replace
|
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,12 @@
|
||||||
|
open System
|
||||||
|
open System.IO
|
||||||
|
open System.Reflection
|
||||||
|
open System.Runtime.InteropServices
|
||||||
open Fli
|
open Fli
|
||||||
|
|
||||||
[<EntryPoint>]
|
type MutationCase = { TestName: string; Patch: string }
|
||||||
let main argv =
|
|
||||||
|
let ensureCleanWorkingDirectory () =
|
||||||
let gitState =
|
let gitState =
|
||||||
cli {
|
cli {
|
||||||
Exec "git"
|
Exec "git"
|
||||||
|
|
@ -12,6 +17,123 @@ let main argv =
|
||||||
|
|
||||||
if gitState.Text <> None then
|
if gitState.Text <> None then
|
||||||
eprintfn "Uncommitted changes. Refusing to run."
|
eprintfn "Uncommitted changes. Refusing to run."
|
||||||
|
exit 2
|
||||||
|
|
||||||
|
let applyPatch patch =
|
||||||
|
cli {
|
||||||
|
Exec "git"
|
||||||
|
Arguments [ "apply"; "-" ]
|
||||||
|
Input patch
|
||||||
|
}
|
||||||
|
|> Command.execute
|
||||||
|
|> Output.throwIfErrored
|
||||||
|
|> ignore
|
||||||
|
|
||||||
|
let restore () =
|
||||||
|
cli {
|
||||||
|
Exec "git"
|
||||||
|
Arguments [ "restore"; "--staged"; "--worktree"; "." ]
|
||||||
|
}
|
||||||
|
|> Command.execute
|
||||||
|
|> Output.throwIfErrored
|
||||||
|
|> ignore
|
||||||
|
|
||||||
|
let ensureBuilt projectPath =
|
||||||
|
cli {
|
||||||
|
Exec "dotnet"
|
||||||
|
Arguments [ "build"; projectPath ]
|
||||||
|
Output(new StreamWriter(Console.OpenStandardOutput()))
|
||||||
|
}
|
||||||
|
|> Command.execute
|
||||||
|
|> Output.throwIfErrored
|
||||||
|
|> ignore
|
||||||
|
|
||||||
|
let runTest projectPath testName =
|
||||||
|
cli {
|
||||||
|
Exec "dotnet"
|
||||||
|
Arguments [ "test"; projectPath; "--filter"; $"FullyQualifiedName={testName}" ]
|
||||||
|
Output(new StreamWriter(Console.OpenStandardOutput()))
|
||||||
|
}
|
||||||
|
|> Command.execute
|
||||||
|
|> ignore
|
||||||
|
|
||||||
|
let getAssemblyPath projectPath =
|
||||||
|
cli {
|
||||||
|
Exec "dotnet"
|
||||||
|
Arguments [ "msbuild"; projectPath; "--getProperty:TargetPath" ]
|
||||||
|
}
|
||||||
|
|> Command.execute
|
||||||
|
|> Output.toText
|
||||||
|
|
||||||
|
let getMetadataLoadContext (assemblyPath: string) =
|
||||||
|
// This allows us to inspect assemblies regardless of the platform that they were built for
|
||||||
|
// https://learn.microsoft.com/en-us/dotnet/standard/assembly/inspect-contents-using-metadataloadcontext
|
||||||
|
let assemblyDir = Path.GetDirectoryName assemblyPath
|
||||||
|
|
||||||
|
let pathAssemblyResolver =
|
||||||
|
[ yield assemblyPath
|
||||||
|
yield! Directory.EnumerateFiles(assemblyDir, "*.dll")
|
||||||
|
yield! Directory.EnumerateFiles(assemblyDir, "*.exe")
|
||||||
|
yield! Directory.GetFiles(RuntimeEnvironment.GetRuntimeDirectory(), "*.dll") ]
|
||||||
|
|> PathAssemblyResolver
|
||||||
|
|
||||||
|
new MetadataLoadContext(pathAssemblyResolver, typeof<obj>.Assembly.GetName().Name)
|
||||||
|
|
||||||
|
let unindented (s: string) =
|
||||||
|
let lines = s.Split([| "\r\n"; "\n" |], StringSplitOptions.None)
|
||||||
|
|
||||||
|
let indexOfFirstNonEmptyLine =
|
||||||
|
lines |> Array.findIndex (not << String.IsNullOrWhiteSpace)
|
||||||
|
|
||||||
|
let identantionOfFirstNonEmptyLine =
|
||||||
|
lines[indexOfFirstNonEmptyLine] |> Seq.takeWhile Char.IsWhiteSpace |> Seq.length
|
||||||
|
|
||||||
|
lines[indexOfFirstNonEmptyLine..]
|
||||||
|
|> Seq.map (fun line -> line.Substring(min identantionOfFirstNonEmptyLine line.Length))
|
||||||
|
|> String.concat Environment.NewLine
|
||||||
|
|
||||||
|
let getMutationCases projectPath =
|
||||||
|
ensureBuilt projectPath
|
||||||
|
|
||||||
|
let assemblyPath = getAssemblyPath projectPath
|
||||||
|
|
||||||
|
use metadataLoadContext = getMetadataLoadContext assemblyPath
|
||||||
|
|
||||||
|
let assemblyTypes =
|
||||||
|
assemblyPath |> metadataLoadContext.LoadFromAssemblyPath |> _.GetTypes()
|
||||||
|
|
||||||
|
let assemblyMethods =
|
||||||
|
assemblyTypes
|
||||||
|
|> Seq.collect _.GetMethods(BindingFlags.Public ||| BindingFlags.Instance)
|
||||||
|
|
||||||
|
assemblyMethods
|
||||||
|
|> Seq.collect (fun m ->
|
||||||
|
m.GetCustomAttributesData()
|
||||||
|
|> Seq.choose (fun attr ->
|
||||||
|
match attr.AttributeType.FullName with
|
||||||
|
| "Mutannot.MutationCaseAttribute" ->
|
||||||
|
Some
|
||||||
|
{ TestName = $"{m.DeclaringType.FullName}.{m.Name}"
|
||||||
|
Patch = attr.ConstructorArguments[0].Value :?> string |> unindented }
|
||||||
|
| _ -> None))
|
||||||
|
|> Seq.toList
|
||||||
|
|
||||||
|
[<EntryPoint>]
|
||||||
|
let main argv =
|
||||||
|
if argv.Length <> 1 then
|
||||||
|
eprintfn "Usage: mutannot <path/to/project.csproj|fsproj>"
|
||||||
exit 1
|
exit 1
|
||||||
|
|
||||||
|
ensureCleanWorkingDirectory ()
|
||||||
|
|
||||||
|
AppDomain.CurrentDomain.ProcessExit.Add(fun _ -> restore ())
|
||||||
|
|
||||||
|
let projectPath = argv[0]
|
||||||
|
|
||||||
|
for mutationCase in getMutationCases projectPath do
|
||||||
|
printfn "MUTATION\n\n%s" <| mutationCase.Patch
|
||||||
|
applyPatch mutationCase.Patch
|
||||||
|
runTest projectPath mutationCase.TestName
|
||||||
|
restore ()
|
||||||
|
|
||||||
0
|
0
|
||||||
|
|
|
||||||
59
flake.lock
generated
59
flake.lock
generated
|
|
@ -1,5 +1,47 @@
|
||||||
{
|
{
|
||||||
"nodes": {
|
"nodes": {
|
||||||
|
"flake-utils": {
|
||||||
|
"inputs": {
|
||||||
|
"systems": "systems"
|
||||||
|
},
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1731533236,
|
||||||
|
"narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
|
||||||
|
"owner": "numtide",
|
||||||
|
"repo": "flake-utils",
|
||||||
|
"rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "numtide",
|
||||||
|
"repo": "flake-utils",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"git-temp-commit": {
|
||||||
|
"inputs": {
|
||||||
|
"flake-utils": [
|
||||||
|
"flake-utils"
|
||||||
|
],
|
||||||
|
"nixpkgs": [
|
||||||
|
"nixpkgs"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1778481212,
|
||||||
|
"narHash": "sha256-Vy0ufQ51CHkamX+XB8hhgohBHJesKli0jF503NuSY20=",
|
||||||
|
"ref": "main",
|
||||||
|
"rev": "622b553f46920a2f3cc92f26c1f49cabb612de5f",
|
||||||
|
"revCount": 2,
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://codeberg.org/svenvanheugten/git-temp-commit.git"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"ref": "main",
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://codeberg.org/svenvanheugten/git-temp-commit.git"
|
||||||
|
}
|
||||||
|
},
|
||||||
"nixpkgs": {
|
"nixpkgs": {
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1776877367,
|
"lastModified": 1776877367,
|
||||||
|
|
@ -18,8 +60,25 @@
|
||||||
},
|
},
|
||||||
"root": {
|
"root": {
|
||||||
"inputs": {
|
"inputs": {
|
||||||
|
"flake-utils": "flake-utils",
|
||||||
|
"git-temp-commit": "git-temp-commit",
|
||||||
"nixpkgs": "nixpkgs"
|
"nixpkgs": "nixpkgs"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"systems": {
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1681028828,
|
||||||
|
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
|
||||||
|
"owner": "nix-systems",
|
||||||
|
"repo": "default",
|
||||||
|
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "nix-systems",
|
||||||
|
"repo": "default",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"root": "root",
|
"root": "root",
|
||||||
|
|
|
||||||
85
flake.nix
85
flake.nix
|
|
@ -1,57 +1,50 @@
|
||||||
{
|
{
|
||||||
inputs = {
|
inputs = {
|
||||||
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
|
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
|
||||||
|
flake-utils.url = "github:numtide/flake-utils";
|
||||||
|
git-temp-commit = {
|
||||||
|
url = "git+https://codeberg.org/svenvanheugten/git-temp-commit.git?ref=main";
|
||||||
|
inputs.nixpkgs.follows = "nixpkgs";
|
||||||
|
inputs.flake-utils.follows = "flake-utils";
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
outputs =
|
outputs =
|
||||||
{ self, nixpkgs }:
|
|
||||||
let
|
|
||||||
systems = [
|
|
||||||
"x86_64-linux"
|
|
||||||
"aarch64-linux"
|
|
||||||
"x86_64-darwin"
|
|
||||||
"aarch64-darwin"
|
|
||||||
];
|
|
||||||
forEachSystem =
|
|
||||||
f: nixpkgs.lib.genAttrs systems (system: f system (import nixpkgs { inherit system; }));
|
|
||||||
in
|
|
||||||
{
|
{
|
||||||
formatter = forEachSystem (_system: pkgs: pkgs.nixfmt-rfc-style);
|
self,
|
||||||
|
nixpkgs,
|
||||||
|
flake-utils,
|
||||||
|
git-temp-commit,
|
||||||
|
}:
|
||||||
|
flake-utils.lib.eachDefaultSystem (
|
||||||
|
system:
|
||||||
|
let
|
||||||
|
pkgs = nixpkgs.legacyPackages.${system};
|
||||||
|
in
|
||||||
|
{
|
||||||
|
packages.default = pkgs.buildDotnetModule {
|
||||||
|
pname = "mutannot";
|
||||||
|
version = "0.1.0";
|
||||||
|
src = ./Mutannot;
|
||||||
|
projectFile = "Mutannot.fsproj";
|
||||||
|
nugetDeps = ./Mutannot/deps.nix;
|
||||||
|
executables = [ "mutannot" ];
|
||||||
|
dotnet-sdk = pkgs.dotnet-sdk_10;
|
||||||
|
dotnet-runtime = pkgs.dotnet-sdk_10;
|
||||||
|
useDotnetFromEnv = true;
|
||||||
|
|
||||||
packages = forEachSystem (
|
meta = {
|
||||||
_system: pkgs:
|
mainProgram = "mutannot";
|
||||||
let
|
|
||||||
mutannot = pkgs.buildDotnetModule {
|
|
||||||
pname = "mutannot";
|
|
||||||
version = "0.1.0";
|
|
||||||
src = ./Mutannot;
|
|
||||||
projectFile = "Mutannot.fsproj";
|
|
||||||
nugetDeps = ./Mutannot/deps.nix;
|
|
||||||
executables = [ "mutannot" ];
|
|
||||||
dotnet-sdk = pkgs.dotnet-sdk_10;
|
|
||||||
dotnet-runtime = pkgs.dotnet-sdk_10;
|
|
||||||
useDotnetFromEnv = true;
|
|
||||||
|
|
||||||
meta = {
|
|
||||||
mainProgram = "mutannot";
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
in
|
};
|
||||||
{
|
|
||||||
default = mutannot;
|
|
||||||
mutannot = mutannot;
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
devShells = forEachSystem (
|
devShells.default = pkgs.mkShell {
|
||||||
_system: pkgs: {
|
packages = [
|
||||||
default = pkgs.mkShell {
|
pkgs.git
|
||||||
packages = [
|
pkgs.dotnet-sdk_10
|
||||||
pkgs.git
|
git-temp-commit.packages.${system}.default
|
||||||
pkgs.dotnet-sdk_10
|
];
|
||||||
];
|
};
|
||||||
};
|
}
|
||||||
}
|
);
|
||||||
);
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue