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/Example.Tests/Calculator.fs b/Example.Tests/Calculator.fs deleted file mode 100644 index cfcce3b..0000000 --- a/Example.Tests/Calculator.fs +++ /dev/null @@ -1,10 +0,0 @@ -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) diff --git a/Example.Tests/CalculatorTests.fs b/Example.Tests/CalculatorTests.fs deleted file mode 100644 index 0a96e10..0000000 --- a/Example.Tests/CalculatorTests.fs +++ /dev/null @@ -1,27 +0,0 @@ -namespace Example.Tests - -open Example -open Mutannot -open Xunit - -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) diff --git a/Example/Calculator.fs b/Example/Calculator.fs new file mode 100644 index 0000000..6f0c515 --- /dev/null +++ b/Example/Calculator.fs @@ -0,0 +1,4 @@ +namespace Example + +module Calculator = + let addOne value = value + 1 diff --git a/Example/CalculatorTests.fs b/Example/CalculatorTests.fs new file mode 100644 index 0000000..9029f0e --- /dev/null +++ b/Example/CalculatorTests.fs @@ -0,0 +1,21 @@ +namespace Example + +open Example +open Mutannot +open Xunit + +type CalculatorTests() = + [] + [] + member _.AddOne_increments() = Assert.Equal(42, Calculator.addOne 41) diff --git a/Example.Tests/Example.Tests.fsproj b/Example/Example.fsproj similarity index 100% rename from Example.Tests/Example.Tests.fsproj rename to Example/Example.fsproj diff --git a/Example.Tests/MutationCaseAttribute.fs b/Example/MutationCaseAttribute.fs similarity index 100% rename from Example.Tests/MutationCaseAttribute.fs rename to Example/MutationCaseAttribute.fs 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 13666be..118570e 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 } @@ -55,7 +56,7 @@ let runTest projectPath testName = Output(new StreamWriter(Console.OpenStandardOutput())) } |> Command.execute - |> ignore + |> Output.toExitCode let getAssemblyPath projectPath = cli { @@ -79,7 +80,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,26 +115,81 @@ 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 +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" + [] let main argv = - if argv.Length <> 1 then - eprintfn "Usage: mutannot " - exit 1 + let parsedArguments = + ArgumentParser.Create(programName = "mutannot") + |> _.ParseCommandLine(argv) + + let projectPath = parsedArguments.GetResult ProjectPath + let validateOnly = parsedArguments.Contains ValidateOnly + let maybeFilter = parsedArguments.TryGetResult Filter ensureCleanWorkingDirectory () AppDomain.CurrentDomain.ProcessExit.Add(fun _ -> restore ()) - let projectPath = argv[0] + 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" + + 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 - for mutationCase in getMutationCases projectPath do - printfn "MUTATION\n\n%s" <| mutationCase.Patch applyPatch mutationCase.Patch - runTest projectPath mutationCase.TestName + + if not validateOnly then + Console.ForegroundColor <- ConsoleColor.Magenta + printf "Output:\n" + Console.ResetColor() + + 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 + + if validateOnly then + printf "Success: All mutantions valid\n" + else + printf "Success: All mutants killed\n" + + Console.ResetColor() + 0 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=" + } +] 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;