Repository navigation
Expand file tree
/
Copy pathMutationTesting.cs
More file actions
77 lines (67 loc) · 3.47 KB
/
Copy pathMutationTesting.cs
File metadata and controls
77 lines (67 loc) · 3.47 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
using System.Linq.Expressions;
namespace CSharpExercises.Testing;
public sealed record Mutant(string Description, bool Killed);
/// <summary>
/// Mutation testing measures how good tests are, not how much code they run: make small deliberate bugs ("mutants"),
/// such as turning < into <= or + into -, and check that some test fails for each. A surviving mutant shows a
/// behaviour no test pins down. Here the code is an expression tree, so mutants are made by rewriting the tree with an
/// <see cref="ExpressionVisitor"/> one operator at a time. Some mutants are "equivalent" (they behave identically), so a
/// score below 100% needs a human look. Tools like Stryker.NET do this to compiled code.
/// </summary>
public static class MutationTesting
{
public static IReadOnlyList<Mutant> Run<T>(Expression<Func<T, T, T>> function, IEnumerable<Func<Func<T, T, T>, bool>> tests)
{
var testList = tests.ToList();
if (!testList.All(t => t(function.Compile()))) throw new InvalidOperationException("The tests must pass on the original code first.");
var mutants = new List<Mutant>();
var count = new OperatorCounter();
count.Visit(function);
for (var i = 0; i < count.Total; i++)
{
var mutator = new SingleMutation(i);
var mutated = (Expression<Func<T, T, T>>)mutator.Visit(function);
if (mutator.Description is null) continue;
var compiled = mutated.Compile();
var killed = testList.Any(t =>
{
try { return !t(compiled); }
catch { return true; } // a mutant that crashes the test is also caught
});
mutants.Add(new Mutant(mutator.Description, killed));
}
return mutants;
}
public static double Score(IReadOnlyList<Mutant> mutants) => mutants.Count == 0 ? 1 : (double)mutants.Count(m => m.Killed) / mutants.Count;
private static readonly Dictionary<ExpressionType, ExpressionType> Swaps = new()
{
[ExpressionType.Add] = ExpressionType.Subtract, [ExpressionType.Subtract] = ExpressionType.Add,
[ExpressionType.Multiply] = ExpressionType.Divide, [ExpressionType.Divide] = ExpressionType.Multiply,
[ExpressionType.LessThan] = ExpressionType.LessThanOrEqual, [ExpressionType.LessThanOrEqual] = ExpressionType.LessThan,
[ExpressionType.GreaterThan] = ExpressionType.GreaterThanOrEqual, [ExpressionType.GreaterThanOrEqual] = ExpressionType.GreaterThan,
};
private sealed class OperatorCounter : ExpressionVisitor
{
public int Total { get; private set; }
protected override Expression VisitBinary(BinaryExpression node)
{
if (Swaps.ContainsKey(node.NodeType)) Total++;
return base.VisitBinary(node);
}
}
/// <summary>Rewrites only the n-th swappable operator, leaving the rest of the tree as it was.</summary>
private sealed class SingleMutation(int target) : ExpressionVisitor
{
private int _seen;
public string? Description { get; private set; }
protected override Expression VisitBinary(BinaryExpression node)
{
if (Swaps.TryGetValue(node.NodeType, out var replacement) && _seen++ == target)
{
Description = $"{node.NodeType} → {replacement} in ({node})";
return Expression.MakeBinary(replacement, Visit(node.Left), Visit(node.Right));
}
return base.VisitBinary(node);
}
}
}