From 5a521e7bfca4f62ec32b378c893887f8168f9fc2 Mon Sep 17 00:00:00 2001 From: Sven van Heugten Date: Tue, 12 May 2026 08:45:31 +0200 Subject: [PATCH 01/10] Actually check if the mutant was killed --- Mutannot/Program.fs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/Mutannot/Program.fs b/Mutannot/Program.fs index 13666be..41dcf33 100644 --- a/Mutannot/Program.fs +++ b/Mutannot/Program.fs @@ -55,7 +55,7 @@ let runTest projectPath testName = Output(new StreamWriter(Console.OpenStandardOutput())) } |> Command.execute - |> ignore + |> Output.toExitCode let getAssemblyPath projectPath = cli { @@ -133,7 +133,13 @@ let main argv = for mutationCase in getMutationCases projectPath do printfn "MUTATION\n\n%s" <| mutationCase.Patch applyPatch mutationCase.Patch - runTest projectPath mutationCase.TestName + + match runTest projectPath mutationCase.TestName with + | 0 -> + eprintfn "Expected tested to fail, but it succeeded" + exit 3 + | _ -> printfn "Mutant killed\n" + restore () 0 From 63d0219e9e1db601c1d6f006a09528ab5bcdb494 Mon Sep 17 00:00:00 2001 From: Sven van Heugten Date: Tue, 12 May 2026 16:02:33 +0200 Subject: [PATCH 02/10] unindented -> unindentPatch --- Mutannot/Program.fs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Mutannot/Program.fs b/Mutannot/Program.fs index 41dcf33..872fc25 100644 --- a/Mutannot/Program.fs +++ b/Mutannot/Program.fs @@ -79,7 +79,7 @@ let getMetadataLoadContext (assemblyPath: string) = new MetadataLoadContext(pathAssemblyResolver, typeof.Assembly.GetName().Name) -let unindented (s: string) = +let unindentPatch (s: string) = let lines = s.Split([| "\r\n"; "\n" |], StringSplitOptions.None) let indexOfFirstNonEmptyLine = @@ -114,7 +114,7 @@ let getMutationCases projectPath = | "Mutannot.MutationCaseAttribute" -> Some { TestName = $"{m.DeclaringType.FullName}.{m.Name}" - Patch = attr.ConstructorArguments[0].Value :?> string |> unindented } + Patch = attr.ConstructorArguments[0].Value :?> string |> unindentPatch } | _ -> None)) |> Seq.toList From ae246d3f36373fd534e576f5f02ce8065f3b6f2a Mon Sep 17 00:00:00 2001 From: Sven van Heugten Date: Tue, 12 May 2026 16:14:35 +0200 Subject: [PATCH 03/10] Improve console output a lot --- Mutannot/Program.fs | 33 ++++++++++++++++++++++++++++----- 1 file changed, 28 insertions(+), 5 deletions(-) diff --git a/Mutannot/Program.fs b/Mutannot/Program.fs index 872fc25..15ba0f2 100644 --- a/Mutannot/Program.fs +++ b/Mutannot/Program.fs @@ -121,7 +121,7 @@ let getMutationCases projectPath = [] let main argv = if argv.Length <> 1 then - eprintfn "Usage: mutannot " + eprintf "Usage: mutannot \n" exit 1 ensureCleanWorkingDirectory () @@ -130,16 +130,39 @@ let main argv = let projectPath = argv[0] - for mutationCase in getMutationCases projectPath do - printfn "MUTATION\n\n%s" <| mutationCase.Patch + for index, mutationCase in getMutationCases projectPath |> Seq.indexed do + Console.ForegroundColor <- ConsoleColor.Green + printf $"MUTATION {index + 1}\n" + + Console.ForegroundColor <- ConsoleColor.Magenta + printf "Test:\n" + Console.ResetColor() + printf "%s\n\n" mutationCase.TestName + + Console.ForegroundColor <- ConsoleColor.Magenta + printf "Patch:\n" + Console.ResetColor() + printf "%s\n" mutationCase.Patch + + Console.ForegroundColor <- ConsoleColor.Magenta + printf "Output:\n" + Console.ResetColor() applyPatch mutationCase.Patch match runTest projectPath mutationCase.TestName with | 0 -> - eprintfn "Expected tested to fail, but it succeeded" + Console.ForegroundColor <- ConsoleColor.Red + eprintf "ERROR: Expected tested to fail, but it succeeded\n" + Console.ResetColor() exit 3 - | _ -> printfn "Mutant killed\n" + | _ -> + Console.ForegroundColor <- ConsoleColor.Green + printf "✓ Mutant killed\n\n" restore () + Console.ForegroundColor <- ConsoleColor.Green + printf "Success: All mutants killed\n" + Console.ResetColor() + 0 From 294b2f85a962bcecc31c140aa17f280d7bef4088 Mon Sep 17 00:00:00 2001 From: Sven van Heugten Date: Tue, 12 May 2026 16:24:19 +0200 Subject: [PATCH 04/10] Add Argu for argument parsing --- Mutannot/Mutannot.fsproj | 1 + Mutannot/Program.fs | 19 ++++++++++++++----- 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/Mutannot/Mutannot.fsproj b/Mutannot/Mutannot.fsproj index 7c6381b..396d322 100644 --- a/Mutannot/Mutannot.fsproj +++ b/Mutannot/Mutannot.fsproj @@ -11,6 +11,7 @@ + diff --git a/Mutannot/Program.fs b/Mutannot/Program.fs index 15ba0f2..0153b3f 100644 --- a/Mutannot/Program.fs +++ b/Mutannot/Program.fs @@ -3,6 +3,7 @@ open System.IO open System.Reflection open System.Runtime.InteropServices open Fli +open Argu type MutationCase = { TestName: string; Patch: string } @@ -118,18 +119,26 @@ let getMutationCases projectPath = | _ -> None)) |> Seq.toList +type Arguments = + | [] ProjectPath of ProjectPath: string + + interface IArgParserTemplate with + member s.Usage = + match s with + | ProjectPath _ -> "path/to/project.csproj|fsproj" + [] let main argv = - if argv.Length <> 1 then - eprintf "Usage: mutannot \n" - exit 1 + let parsedArguments = + ArgumentParser.Create(programName = "mutannot") + |> _.ParseCommandLine(argv) + + let projectPath = parsedArguments.GetResult ProjectPath ensureCleanWorkingDirectory () AppDomain.CurrentDomain.ProcessExit.Add(fun _ -> restore ()) - let projectPath = argv[0] - for index, mutationCase in getMutationCases projectPath |> Seq.indexed do Console.ForegroundColor <- ConsoleColor.Green printf $"MUTATION {index + 1}\n" From 500b3f0d8ce74c0d4f61c573f67fcf2ad89a7b96 Mon Sep 17 00:00:00 2001 From: Sven van Heugten Date: Tue, 12 May 2026 16:30:23 +0200 Subject: [PATCH 05/10] Introduce --validateonly --- Mutannot/Program.fs | 34 ++++++++++++++++++++++------------ 1 file changed, 22 insertions(+), 12 deletions(-) diff --git a/Mutannot/Program.fs b/Mutannot/Program.fs index 0153b3f..e77c630 100644 --- a/Mutannot/Program.fs +++ b/Mutannot/Program.fs @@ -121,11 +121,13 @@ let getMutationCases projectPath = type Arguments = | [] ProjectPath of ProjectPath: string + | ValidateOnly interface IArgParserTemplate with member s.Usage = match s with | ProjectPath _ -> "path/to/project.csproj|fsproj" + | ValidateOnly -> "check if the patches apply, but don't run the mutations" [] let main argv = @@ -134,6 +136,7 @@ let main argv = |> _.ParseCommandLine(argv) let projectPath = parsedArguments.GetResult ProjectPath + let validateOnly = parsedArguments.Contains ValidateOnly ensureCleanWorkingDirectory () @@ -153,25 +156,32 @@ let main argv = Console.ResetColor() printf "%s\n" mutationCase.Patch - Console.ForegroundColor <- ConsoleColor.Magenta - printf "Output:\n" - Console.ResetColor() applyPatch mutationCase.Patch - match runTest projectPath mutationCase.TestName with - | 0 -> - Console.ForegroundColor <- ConsoleColor.Red - eprintf "ERROR: Expected tested to fail, but it succeeded\n" + if not validateOnly then + Console.ForegroundColor <- ConsoleColor.Magenta + printf "Output:\n" Console.ResetColor() - exit 3 - | _ -> - Console.ForegroundColor <- ConsoleColor.Green - printf "✓ Mutant killed\n\n" + + match runTest projectPath mutationCase.TestName with + | 0 -> + Console.ForegroundColor <- ConsoleColor.Red + eprintf "ERROR: Expected tested to fail, but it succeeded\n" + Console.ResetColor() + exit 3 + | _ -> + Console.ForegroundColor <- ConsoleColor.Green + printf "✓ Mutant killed\n\n" restore () Console.ForegroundColor <- ConsoleColor.Green - printf "Success: All mutants killed\n" + + if validateOnly then + printf "Success: All mutantions valid\n" + else + printf "Success: All mutants killed\n" + Console.ResetColor() 0 From 9ff53f18035b14ba208dfcf1ef0f6cb3532d730b Mon Sep 17 00:00:00 2001 From: Sven van Heugten Date: Tue, 12 May 2026 16:31:05 +0200 Subject: [PATCH 06/10] Remove unnecessary functions in example --- Example.Tests/Calculator.fs | 8 +------- Example.Tests/CalculatorTests.fs | 10 ++-------- 2 files changed, 3 insertions(+), 15 deletions(-) diff --git a/Example.Tests/Calculator.fs b/Example.Tests/Calculator.fs index cfcce3b..030e391 100644 --- a/Example.Tests/Calculator.fs +++ b/Example.Tests/Calculator.fs @@ -1,10 +1,4 @@ namespace Example module Calculator = - let addOne value = value + 1 - - let absoluteDifference left right = - if left >= right then left - right else right - left - - let isLeapYear year = - year % 4 = 0 && (year % 100 <> 0 || year % 400 = 0) + let addOne value = value - 1 diff --git a/Example.Tests/CalculatorTests.fs b/Example.Tests/CalculatorTests.fs index 0a96e10..1555082 100644 --- a/Example.Tests/CalculatorTests.fs +++ b/Example.Tests/CalculatorTests.fs @@ -8,20 +8,14 @@ type CalculatorTests() = [] [= right then left - right else right - left - - member _.AddOne_increments() = - Assert.Equal(42, Calculator.addOne 41) """)>] member _.AddOne_increments() = Assert.Equal(42, Calculator.addOne 41) From de5843a783c26a60e62f0a0ad10d5b82252b034c Mon Sep 17 00:00:00 2001 From: Sven van Heugten Date: Tue, 12 May 2026 16:34:08 +0200 Subject: [PATCH 07/10] Simplify Example --- {Example.Tests => Example}/Calculator.fs | 2 +- {Example.Tests => Example}/CalculatorTests.fs | 8 ++++---- .../Example.Tests.fsproj => Example/Example.fsproj | 0 {Example.Tests => Example}/MutationCaseAttribute.fs | 0 4 files changed, 5 insertions(+), 5 deletions(-) rename {Example.Tests => Example}/Calculator.fs (54%) rename {Example.Tests => Example}/CalculatorTests.fs (67%) rename Example.Tests/Example.Tests.fsproj => Example/Example.fsproj (100%) rename {Example.Tests => Example}/MutationCaseAttribute.fs (100%) diff --git a/Example.Tests/Calculator.fs b/Example/Calculator.fs similarity index 54% rename from Example.Tests/Calculator.fs rename to Example/Calculator.fs index 030e391..6f0c515 100644 --- a/Example.Tests/Calculator.fs +++ b/Example/Calculator.fs @@ -1,4 +1,4 @@ namespace Example module Calculator = - let addOne value = value - 1 + let addOne value = value + 1 diff --git a/Example.Tests/CalculatorTests.fs b/Example/CalculatorTests.fs similarity index 67% rename from Example.Tests/CalculatorTests.fs rename to Example/CalculatorTests.fs index 1555082..9029f0e 100644 --- a/Example.Tests/CalculatorTests.fs +++ b/Example/CalculatorTests.fs @@ -1,4 +1,4 @@ -namespace Example.Tests +namespace Example open Example open Mutannot @@ -7,10 +7,10 @@ open Xunit type CalculatorTests() = [] [ Date: Tue, 12 May 2026 16:38:47 +0200 Subject: [PATCH 08/10] Introduce --filter --- Mutannot/Program.fs | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/Mutannot/Program.fs b/Mutannot/Program.fs index e77c630..118570e 100644 --- a/Mutannot/Program.fs +++ b/Mutannot/Program.fs @@ -121,12 +121,14 @@ let getMutationCases projectPath = type Arguments = | [] ProjectPath of ProjectPath: string + | Filter of SearchString: string | ValidateOnly interface IArgParserTemplate with member s.Usage = match s with | ProjectPath _ -> "path/to/project.csproj|fsproj" + | Filter _ -> "filter down to mutations that contain the given search string" | ValidateOnly -> "check if the patches apply, but don't run the mutations" [] @@ -137,12 +139,18 @@ let main argv = let projectPath = parsedArguments.GetResult ProjectPath let validateOnly = parsedArguments.Contains ValidateOnly + let maybeFilter = parsedArguments.TryGetResult Filter ensureCleanWorkingDirectory () AppDomain.CurrentDomain.ProcessExit.Add(fun _ -> restore ()) - for index, mutationCase in getMutationCases projectPath |> Seq.indexed do + let filteredMutations = + getMutationCases projectPath + |> Seq.filter _.Patch.Contains(maybeFilter |> Option.defaultValue "") + |> Seq.indexed + + for index, mutationCase in filteredMutations do Console.ForegroundColor <- ConsoleColor.Green printf $"MUTATION {index + 1}\n" From cf8f914df01f7dbc5aed346bb10a7d3365f727a1 Mon Sep 17 00:00:00 2001 From: Sven van Heugten Date: Tue, 12 May 2026 16:44:42 +0200 Subject: [PATCH 09/10] Update deps.json --- Mutannot/deps.json | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 Mutannot/deps.json diff --git a/Mutannot/deps.json b/Mutannot/deps.json new file mode 100644 index 0000000..8d83a9c --- /dev/null +++ b/Mutannot/deps.json @@ -0,0 +1,27 @@ +[ + { + "pname": "Argu", + "version": "6.2.5", + "hash": "sha256-5HcZcvco4e8+hgLhzlxk7ZmFVLtZL9LVr7LbmXsLmNU=" + }, + { + "pname": "Fli", + "version": "1.1000.0", + "hash": "sha256-LKJ2raQJuNfJKOA6Y85tECMnUFuKsmd5fBOG2Sq5OjY=" + }, + { + "pname": "System.Configuration.ConfigurationManager", + "version": "4.4.0", + "hash": "sha256-+8wGYllXnIxRzy9dLhZFB88GoPj8ivYXS0KUfcivT8I=" + }, + { + "pname": "System.Reflection.MetadataLoadContext", + "version": "9.0.1", + "hash": "sha256-kWm31a0unw/H8SjxaabVYKInR40bTAL9JnGQEVQGTsU=" + }, + { + "pname": "System.Security.Cryptography.ProtectedData", + "version": "4.4.0", + "hash": "sha256-Ri53QmFX8I8UH0x4PikQ1ZA07ZSnBUXStd5rBfGWFOE=" + } +] From c13b257083911b08c0c593c6f79bc73b88641361 Mon Sep 17 00:00:00 2001 From: Sven van Heugten Date: Tue, 12 May 2026 16:47:24 +0200 Subject: [PATCH 10/10] nix stuff --- .gitignore | 3 +++ Mutannot/deps.nix | 8 -------- flake.nix | 2 +- 3 files changed, 4 insertions(+), 9 deletions(-) delete mode 100644 Mutannot/deps.nix diff --git a/.gitignore b/.gitignore index 33afe2c..2b749db 100644 --- a/.gitignore +++ b/.gitignore @@ -211,3 +211,6 @@ FakesAssemblies/ # Codex .codex + +# Nix +result diff --git a/Mutannot/deps.nix b/Mutannot/deps.nix deleted file mode 100644 index 60de7f1..0000000 --- a/Mutannot/deps.nix +++ /dev/null @@ -1,8 +0,0 @@ -{ fetchNuGet }: -[ - (fetchNuGet { - pname = "FSharp.Core"; - version = "10.1.201"; - sha256 = "sha256-NzxdRJgL+5RQpUm8Y6Mc0w7sakxqThv6qHpP+u0x5x0="; - }) -] diff --git a/flake.nix b/flake.nix index f3fe06f..f72685d 100644 --- a/flake.nix +++ b/flake.nix @@ -27,7 +27,7 @@ version = "0.1.0"; src = ./Mutannot; projectFile = "Mutannot.fsproj"; - nugetDeps = ./Mutannot/deps.nix; + nugetDeps = ./Mutannot/deps.json; executables = [ "mutannot" ]; dotnet-sdk = pkgs.dotnet-sdk_10; dotnet-runtime = pkgs.dotnet-sdk_10;