Skip to content

Commit

Permalink
Context & Colors (#9)
Browse files Browse the repository at this point in the history
  • Loading branch information
tuomohopia authored Nov 18, 2023
1 parent 8207afe commit 304dbaf
Show file tree
Hide file tree
Showing 3 changed files with 101 additions and 26 deletions.
9 changes: 5 additions & 4 deletions lib/mix/mutate.ex
Original file line number Diff line number Diff line change
Expand Up @@ -131,9 +131,10 @@ defmodule Mix.Tasks.Mutate do

# Internal

defp do_run(source_file, test_files, mutation) do
defp do_run(source_path, test_files, mutation) do
# Get source file's AST
ast = source_file |> File.read!() |> Code.string_to_quoted!()
source_content = File.read!(source_path)
ast = Code.string_to_quoted!(source_content)
{:ok, test_modules, _compilation_metas} = Kernel.ParallelCompiler.require(test_files, [])

ExUnit.Server.modules_loaded(false)
Expand Down Expand Up @@ -173,12 +174,12 @@ defmodule Mix.Tasks.Mutate do

"""
No ( #{from} ) operators found in #{source_file}.
No ( #{from} ) operators found in #{source_path}.
Thus, no mutants injected.
"""
else
Mutix.mutation_report(test_results, source_file, mutation)
Mutix.mutation_report(test_results, {source_path, source_content}, mutation)
end

IO.puts(mutation_report)
Expand Down
69 changes: 57 additions & 12 deletions lib/mutix/report.ex
Original file line number Diff line number Diff line change
Expand Up @@ -11,29 +11,37 @@ defmodule Mutix.Report do
@typep mutation_test_result ::
{result :: exunit_test_result(), meta :: Keyword.t(), io_output :: String.t()}

@spec mutation(list(mutation_test_result()), String.t(), {atom(), atom()}) :: String.t()
def mutation(test_results, source_file_path, operator_mutation) do
@spec mutation(list(mutation_test_result()), {String.t(), String.t()}, {atom(), atom()}) ::
String.t()
def mutation(test_results, {source_file_path, source_content}, operator_mutation) do
{from, to} = operator_mutation
score = mutation_score(test_results)

percentage = (score.mutant_count - score.survived_count) / score.mutant_count * 100
percentage = Float.round(percentage, 1)

percentage =
if percentage > 50,
do: color(percentage, [:green, :bright]),
else: color(percentage, [:red, :bright])

survived = score.survived

survived_report =
if Enum.count(survived) > 0,
do: survived_report(survived, source_file_path, operator_mutation),
do: survived_report(survived, {source_file_path, source_content}, operator_mutation),
else: nil

"""
Results:
#{color("Results:", [:cyan, :bright])}
#{score.mutant_count} mutants were generated by mutating ( #{from} ) into ( #{to} ).
#{color(score.mutant_count, [:blue, :bright])} mutants were generated by mutating ( #{color(from, :green)} ) into ( #{color(to, :red)} ).
#{score.test_count} tests were run for each mutant.
#{color(score.test_count, [:blue, :bright])} tests were run for each mutant.
#{score.killed_count} / #{score.mutant_count} mutants killed by the test suite.
#{color(score.killed_count, :green)} / #{color(score.mutant_count, [:blue, :bright])} mutants killed by the test suite.
Mutation score: #{Float.round(percentage, 1)} %
Mutation score: #{percentage} %
#{survived_report}
"""
end
Expand All @@ -59,24 +67,61 @@ defmodule Mutix.Report do
}
end

defp survived_report(survived, source_file_path, {from, to}) do
defp survived_report(survived, {source_file_path, source_content}, {from, to}) do
lines = String.split(source_content, "\n")

surviving =
for {_, meta, _} <- survived do
line = Keyword.fetch!(meta, :line)
index = Keyword.fetch!(meta, :index_on_line)

context =
[
Enum.at(lines, line - 2),
Enum.at(lines, line - 1),
Enum.at(lines, line)
]
|> trim_context()
|> Enum.map(&String.pad_leading(&1, String.length(&1) + 8))
# TODO: color only the operator here, not the entire line
|> List.update_at(1, fn line -> color(line, :red) end)
|> Enum.join("\n")

if index == 0 do
"#{source_file_path} - line #{line} where ( #{from} ) was mutated into ( #{to} )"
"""
#{color(source_file_path, :magenta)}:#{color(line, :magenta)} where ( #{color(from, :green)} ) was mutated into ( #{color(to, :red)} ):
#{context}
"""
else
"#{source_file_path} - line #{line} where the #{index + 1}. ( #{from} ) from left was mutated into ( #{to} )"
"""
#{source_file_path} - line #{line} where the #{index + 1}. ( #{color(from, :green)} ) from left was mutated into ( #{color(to, :red)} )
#{context}
"""
end
end

"""
Surviving mutants (no test failed with these injections):
#{color("Surviving mutants", [:magenta, :bright])} - no test failed with these injections:
#{Enum.join(surviving, "\n ")}
"""
end

defp color(value, ansi), do: IO.ANSI.format([ansi | to_string(value)])

defp trim_context(lines) do
trimmed_lines = Enum.map(lines, &String.trim/1)

min_whitespace =
lines
|> Enum.zip_with(trimmed_lines, fn line, trimmed ->
String.length(line) - String.length(trimmed)
end)
|> Enum.min()

Enum.map(lines, fn line -> String.slice(line, min_whitespace..-1) end)
end
end
49 changes: 39 additions & 10 deletions test/mutix/report_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -8,22 +8,32 @@ defmodule Mutix.ReportTest do
@single_operator_result """
Results:
1 mutants were generated by mutating ( > ) into ( < ).
1 mutants were generated by mutating ( > ) into ( < ).
5 tests were run for each mutant.
5 tests were run for each mutant.
0 / 1 mutants killed by the test suite.
0 / 1 mutants killed by the test suite.
Mutation score: 0.0 %
Mutation score: 0.0 %
Surviving mutants (no test failed with these injections):
Surviving mutants - no test failed with these injections:
test/support/single_operator_source.ex - line 3 where ( > ) was mutated into ( < )
test/support/single_operator_source.ex:3 where ( > ) was mutated into ( < ):
def larger_than_1(a) do
a > 1
end
"""

ExUnit.after_suite(fn _result -> Application.put_env(:elixir, :ansi_enabled, true) end)

describe "mutation/3" do
setup do
Application.put_env(:elixir, :ansi_enabled, false)
end

test "produces a mutation report for the output of a list of mutation test suites" do
test_results = [
{%{total: 5, failures: 0, excluded: 0, skipped: 0}, [index_on_line: 0, line: 3],
Expand All @@ -33,11 +43,24 @@ defmodule Mutix.ReportTest do
source_file_path = "test/support/single_operator_source.ex"
operator_mutation = {:>, :<}

result = Report.mutation(test_results, source_file_path, operator_mutation)
assert result == String.replace_trailing(@single_operator_result, "\n\n\n", "\n\n")
result =
Report.mutation(
test_results,
{source_file_path, File.read!(source_file_path)},
operator_mutation
)

result = result |> String.split("\n") |> Enum.map(&String.trim/1)

single_operator_result =
@single_operator_result |> String.split("\n") |> Enum.map(&String.trim/1)

assert result == single_operator_result
end

test "reports mutation score correctly" do
test "reports mutation score correctly with context" do
Application.put_env(:elixir, :ansi_enabled, false)

test_results = [
{%{total: 5, failures: 0, excluded: 0, skipped: 0}, [index_on_line: 0, line: 3], ""},
{%{total: 5, failures: 1, excluded: 0, skipped: 0}, [index_on_line: 0, line: 5], ""},
Expand All @@ -47,7 +70,13 @@ defmodule Mutix.ReportTest do
source_file_path = "test/support/single_operator_source.ex"
operator_mutation = {:>, :<}

result = Report.mutation(test_results, source_file_path, operator_mutation)
result =
Report.mutation(
test_results,
{source_file_path, File.read!(source_file_path)},
operator_mutation
)

assert result =~ "5 tests were run for each mutant."
assert result =~ "2 / 3 mutants killed by the test suite."
assert result =~ "Mutation score: 66.7 %"
Expand Down

0 comments on commit 304dbaf

Please sign in to comment.