Skip to content

Commit

Permalink
feat(mutator): Add conditional operator mutator (#2583)
Browse files Browse the repository at this point in the history
* feat(Mutators): Add conditional operator mutator

Add conditional operator mutator
Add tests for conditional operator mutator
Include conditional operator mutator in mutations.md

* Fixes to unit tests

* feat(Mutators): Remove NegateConditionMutator's conditional expression mutation

* Fix integration tests

* Fix last integration test

* ConditionalExpressionMutator should not mutate declaration patterns. Fix tests.

* Fix ValidateStrykerResults compilation.

---------

Co-authored-by: Michał Isalski <[email protected]>
Co-authored-by: Liam Rougoor <[email protected]>
Co-authored-by: Liam Rougoor <[email protected]>
Co-authored-by: Rouke Broersma <[email protected]>
  • Loading branch information
5 people authored Jun 7, 2024
1 parent c68c46c commit 6556ab9
Show file tree
Hide file tree
Showing 10 changed files with 185 additions and 61 deletions.
6 changes: 6 additions & 0 deletions docs/mutations.md
Original file line number Diff line number Diff line change
Expand Up @@ -256,3 +256,9 @@ For the full list of all available regex mutations, see the [regex mutator docs]
| `a ?? b` | `b ?? a` |
| `a ?? b` | `a` |
| `a ?? b` | `b` |

## Conditional Operators (_conditional_)
| Original | Mutated |
|---------------------|---------------------|
| `x ? a : b` | `true ? a : b` |
| `x ? a : b` | `false ? a : b` |
10 changes: 4 additions & 6 deletions integrationtest/ValidationProject/ValidateStrykerResults.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,12 @@
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.Text;
using Newtonsoft.Json;
using Shouldly;
using Stryker.Core.Mutants;
using Stryker.Core.Reporters.Json;
using Stryker.CLI;
using Xunit;

namespace IntegrationTests
Expand Down Expand Up @@ -63,7 +61,7 @@ public void NetFullFramework()

var report = JsonConvert.DeserializeObject<JsonReport>(strykerRunOutput);

CheckReportMutants(report, total: 28, ignored: 7, survived: 2, killed: 7, timeout: 0, nocoverage: 11);
CheckReportMutants(report, total: 29, ignored: 7, survived: 3, killed: 7, timeout: 0, nocoverage: 11);
}
}

Expand All @@ -82,7 +80,7 @@ public void NetCore()

var report = JsonConvert.DeserializeObject<JsonReport>(strykerRunOutput);

CheckReportMutants(report, total: 117, ignored: 57, survived: 4, killed: 7, timeout: 2, nocoverage: 45);
CheckReportMutants(report, total: 120, ignored: 58, survived: 4, killed: 8, timeout: 2, nocoverage: 46);
CheckReportTestCounts(report, total: 16);
}

Expand Down Expand Up @@ -121,7 +119,7 @@ public void NetCoreWithTwoTestProjects()

var report = JsonConvert.DeserializeObject<JsonReport>(strykerRunOutput);

CheckReportMutants(report, total: 117, ignored: 28, survived: 8, killed: 10, timeout: 2, nocoverage: 67);
CheckReportMutants(report, total: 120, ignored: 29, survived: 8, killed: 11, timeout: 2, nocoverage: 68);
CheckReportTestCounts(report, total: 32);
}

Expand All @@ -140,7 +138,7 @@ public void SolutionRun()

var report = JsonConvert.DeserializeObject<JsonReport>(strykerRunOutput);

CheckReportMutants(report, total: 117, ignored: 57, survived: 4, killed: 7, timeout: 2, nocoverage: 45);
CheckReportMutants(report, total: 120, ignored: 58, survived: 4, killed: 8, timeout: 2, nocoverage: 46);
CheckReportTestCounts(report, total: 32);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,7 @@ public void ShouldAddReturnDefaultToArrowExpressionOperator()
string source =
@"public static int operator+ (TestClass value, TestClass other) => Sub(out var x, """")?1:2;";
string expected =
@"public static int operator+ (TestClass value, TestClass other) {if(StrykerNamespace.MutantControl.IsActive(0)){return!(Sub(out var x, """"))?1:2;}else{return Sub(out var x, (StrykerNamespace.MutantControl.IsActive(1)?""Stryker was here!"":""""))?1:2;}}";
@"public static int operator+ (TestClass value, TestClass other) {if(StrykerNamespace.MutantControl.IsActive(1)){return(false?1:2);}else{if(StrykerNamespace.MutantControl.IsActive(0)){return(true?1:2);}else{return Sub(out var x, (StrykerNamespace.MutantControl.IsActive(2)?""Stryker was here!"":""""))?1:2;}}}";
ShouldMutateSourceInClassToExpected(source, expected);
}

Expand Down Expand Up @@ -156,8 +156,8 @@ public void ShouldMutateExpressionBodiedLocalFunction()
int SomeMethod() => (true && SomeOtherMethod(out var x)) ? x : 5;
}";
string expected = @"void TestMethod(){if(StrykerNamespace.MutantControl.IsActive(0)){}else{
int SomeMethod() {if(StrykerNamespace.MutantControl.IsActive(1)){return!((true && SomeOtherMethod(out var x)) )? x : 5;}else{if(StrykerNamespace.MutantControl.IsActive(2)){return(true || SomeOtherMethod(out var x)) ? x : 5;}else{return ((StrykerNamespace.MutantControl.IsActive(3)?false:true )&& SomeOtherMethod(out var x)) ? x : 5;}}};}}
}";
int SomeMethod() {if(StrykerNamespace.MutantControl.IsActive(2)){return(false?x :5);}else{if(StrykerNamespace.MutantControl.IsActive(1)){return(true?x :5);}else{if(StrykerNamespace.MutantControl.IsActive(3)){return(true || SomeOtherMethod(out var x)) ? x : 5;}else{return((StrykerNamespace.MutantControl.IsActive(4)?false:true )&& SomeOtherMethod(out var x)) ? x : 5;}}}};
}}";

ShouldMutateSourceInClassToExpected(source, expected);
}
Expand Down Expand Up @@ -355,28 +355,22 @@ private bool Out(out string test)
return true;
}";
string expected = @"void TestMethod()
{if(StrykerNamespace.MutantControl.IsActive(0)){}else{if(StrykerNamespace.MutantControl.IsActive(1)){
{if(StrykerNamespace.MutantControl.IsActive(0)){}else{if(StrykerNamespace.MutantControl.IsActive(2)){
int i = 0;
var result = !(Out(out var test) )? test : """";
var result = (false?test :"""");
}
else{
else{if(StrykerNamespace.MutantControl.IsActive(1)){
int i = 0;
var result = Out(out var test) ? test : (StrykerNamespace.MutantControl.IsActive(2)?""Stryker was here!"":"""");
}
}
var result = (true?test :"""");
}
private bool Out(out string test)
{
{ test = default(string); }
if (StrykerNamespace.MutantControl.IsActive(3))
{ }
else
{
return (StrykerNamespace.MutantControl.IsActive(4) ? false : true);
}
return default(bool);
else{
int i = 0;
var result = Out(out var test) ? test : (StrykerNamespace.MutantControl.IsActive(3)?""Stryker was here!"":"""");
}
";
}}}private bool Out(out string test)
{{test= default(string);}if(StrykerNamespace.MutantControl.IsActive(4)){}else{
return (StrykerNamespace.MutantControl.IsActive(5)?false:true);
}return default(bool);}";

ShouldMutateSourceInClassToExpected(source, expected);
}
Expand All @@ -397,19 +391,13 @@ private bool Out(int test, Func<int, bool>lambda )
string expected = @"void TestMethod()
{if(StrykerNamespace.MutantControl.IsActive(0)){}else{
int i = 0;
var result = (StrykerNamespace.MutantControl.IsActive(1)?!(Out(i, (x) => { int.TryParse(""3"", out int y); return x == y;} ) ):Out(i, (x) => {if(StrykerNamespace.MutantControl.IsActive(2)){}else{ int.TryParse((StrykerNamespace.MutantControl.IsActive(3)?"""":""3""), out int y); return (StrykerNamespace.MutantControl.IsActive(4)?x != y:x == y);} return default;}) )? i.ToString() : (StrykerNamespace.MutantControl.IsActive(5)?""Stryker was here!"":"""");
var result = (StrykerNamespace.MutantControl.IsActive(2)?(false?i.ToString() :""""):(StrykerNamespace.MutantControl.IsActive(1)?(true?i.ToString() :""""):Out(i, (x) => {if(StrykerNamespace.MutantControl.IsActive(3)){}else{ int.TryParse((StrykerNamespace.MutantControl.IsActive(4)?"""":""3""), out int y); return (StrykerNamespace.MutantControl.IsActive(5)?x != y:x == y);} return default;}) ? i.ToString() : (StrykerNamespace.MutantControl.IsActive(6)?""Stryker was here!"":"""")));
}
}
private bool Out(int test, Func<int, bool> lambda)
{
if (StrykerNamespace.MutantControl.IsActive(6))
{ }
else
{
return (StrykerNamespace.MutantControl.IsActive(7) ? false : true);
}
return default(bool);
}";
}private bool Out(int test, Func<int, bool>lambda )
{if(StrykerNamespace.MutantControl.IsActive(7)){}else{
return (StrykerNamespace.MutantControl.IsActive(8)?false:true);
}
return default(bool);}";

ShouldMutateSourceInClassToExpected(source, expected);
}
Expand All @@ -426,9 +414,9 @@ public void ShouldMutateWhenDeclarationInInnerScopeInExpressionForm()
var expected = @"void TestMethod()
{if(StrykerNamespace.MutantControl.IsActive(0)){}else{
int i = 0;
var result = Out(i, (x) => {if(StrykerNamespace.MutantControl.IsActive(1)){return!(int.TryParse(""3"", out int y) )? true : false;}else{return int.TryParse((StrykerNamespace.MutantControl.IsActive(2)?"""":""3""), out int y) ? (StrykerNamespace.MutantControl.IsActive(3)?false:true ): (StrykerNamespace.MutantControl.IsActive(4)?true:false);}});
}}
";
var result = Out(i, (x) => (StrykerNamespace.MutantControl.IsActive(2)?(false?true :false):(StrykerNamespace.MutantControl.IsActive(1)?(true?true :false):int.TryParse((StrykerNamespace.MutantControl.IsActive(3)?"""":""3""), out int y) ? (StrykerNamespace.MutantControl.IsActive(4)?false:true ): (StrykerNamespace.MutantControl.IsActive(5)?true:false))));
}
}";

ShouldMutateSourceInClassToExpected(source, expected);
}
Expand Down Expand Up @@ -494,7 +482,7 @@ string SomeLocalFunction()

ShouldMutateSourceInClassToExpected(source, expected);
}

[Fact]
public void ShouldMutateConditionalMemberAccessProperly()
{
Expand All @@ -509,7 +497,7 @@ public void ShouldMutateConditionalMemberAccessProperly()

ShouldMutateSourceInClassToExpected(source, expected);
}


[Fact]
public void ShouldMutateConditionalExpressionOnArrayDeclaration()
Expand Down Expand Up @@ -742,17 +730,17 @@ public void ShouldMutateComplexExpressionBodiedMethod()
{
string source = @"public int SomeMethod() => (true && SomeOtherMethod(out var x)) ? x : 5;";
string expected =
@"public int SomeMethod() {if(StrykerNamespace.MutantControl.IsActive(0)){return!((true && SomeOtherMethod(out var x)) )? x : 5;}else{if(StrykerNamespace.MutantControl.IsActive(1)){return(true || SomeOtherMethod(out var x)) ? x : 5;}else{return ((StrykerNamespace.MutantControl.IsActive(2)?false:true )&& SomeOtherMethod(out var x)) ? x : 5;}}}";
@"public int SomeMethod() {if(StrykerNamespace.MutantControl.IsActive(1)){return(false?x :5);}else{if(StrykerNamespace.MutantControl.IsActive(0)){return(true?x :5);}else{if(StrykerNamespace.MutantControl.IsActive(2)){return(true || SomeOtherMethod(out var x)) ? x : 5;}else{return ((StrykerNamespace.MutantControl.IsActive(3)?false:true )&& SomeOtherMethod(out var x)) ? x : 5;}}}}";

ShouldMutateSourceInClassToExpected(source, expected);
}

[Fact]
public void ShouldMutateExpressionBodiedStaticConstructor()
{
string source = @"static Test() => (true && SomeOtherMethod(out var x)) ? x : 5;";
string source = @"static Test() => (true && SomeOtherMethod(out var x)) ? x : 5;";
string expected =
@"static Test() {using(new StrykerNamespace.MutantContext()){if(StrykerNamespace.MutantControl.IsActive(0)){!((true && SomeOtherMethod(out var x)) )? x : 5;}else{if(StrykerNamespace.MutantControl.IsActive(1)){(true || SomeOtherMethod(out var x)) ? x : 5;}else{((StrykerNamespace.MutantControl.IsActive(2)?false:true )&& SomeOtherMethod(out var x)) ? x : 5;}}}}}";
@"static Test() {using(new StrykerNamespace.MutantContext()){if(StrykerNamespace.MutantControl.IsActive(1)){(false?x :5);}else{if(StrykerNamespace.MutantControl.IsActive(0)){(true?x :5);}else{if(StrykerNamespace.MutantControl.IsActive(2)){(true || SomeOtherMethod(out var x)) ? x : 5;}else{((StrykerNamespace.MutantControl.IsActive(3)?false:true )&& SomeOtherMethod(out var x)) ? x : 5;}}}}}";

ShouldMutateSourceInClassToExpected(source, expected);
}
Expand Down Expand Up @@ -1552,12 +1540,29 @@ public void ShouldMutatePropertiesInArrowFormEvenWithComplexConstruction()
static TestClass(){}}";

var expected = @"class Test {
string Value {get {if(StrykerNamespace.MutantControl.IsActive(0)){return!(Out(out var x))? ""empty"": """";}else{return Out(out var x)? (StrykerNamespace.MutantControl.IsActive(1)?"""":""empty""): (StrykerNamespace.MutantControl.IsActive(2)?""Stryker was here!"":"""");}}}
string Value {get {if(StrykerNamespace.MutantControl.IsActive(1)){return(false?""empty"":"""");}else{if(StrykerNamespace.MutantControl.IsActive(0)){return(true?""empty"":"""");}else{return Out(out var x)? (StrykerNamespace.MutantControl.IsActive(2)?"""":""empty""): (StrykerNamespace.MutantControl.IsActive(3)?""Stryker was here!"":"""");}}}}
static TestClass(){using(new StrykerNamespace.MutantContext()){}}}";

ShouldMutateSourceInClassToExpected(source, expected);
}

[Fact]
public void ShouldMutateConditionalExpression()
{
string source = @"void TestMethod()
{
var a = 1;
var x = a == 1 ? 5 : 7;
}";
string expected = @"void TestMethod()
{if(StrykerNamespace.MutantControl.IsActive(0)){}else{
var a = 1;
var x = (StrykerNamespace.MutantControl.IsActive(2)?(false?5 :7):(StrykerNamespace.MutantControl.IsActive(1)?(true?5 :7):(StrykerNamespace.MutantControl.IsActive(3)?a != 1 :a == 1 )? 5 : 7));
}}";

ShouldMutateSourceInClassToExpected(source, expected);
}

[Fact]
public void ShouldMutateStaticProperties()
{
Expand Down Expand Up @@ -1601,7 +1606,7 @@ public void ShouldMutateStaticPropertiesInArrowFormEvenWithComplexConstruction()
static string Value => Out(out var x)? ""empty"": """";}";

var expected = @"class Test {
static string Value {get {if(StrykerNamespace.MutantControl.IsActive(0)){return!(Out(out var x))? ""empty"": """";}else{return Out(out var x)? (StrykerNamespace.MutantControl.IsActive(1)?"""":""empty""): (StrykerNamespace.MutantControl.IsActive(2)?""Stryker was here!"":"""");}}}}";
static string Value {get{if(StrykerNamespace.MutantControl.IsActive(1)){return(false?""empty"":"""");}else{if(StrykerNamespace.MutantControl.IsActive(0)){return(true?""empty"":"""");}else{return Out(out var x)? (StrykerNamespace.MutantControl.IsActive(2)?"""":""empty""): (StrykerNamespace.MutantControl.IsActive(3)?""Stryker was here!"":"""");}}}}}";

ShouldMutateSourceInClassToExpected(source, expected);
}
Expand All @@ -1614,8 +1619,7 @@ public void ShouldMutatePropertiesAsArrowExpression()
}";

var expected = @"class Test {
string Value {get{if(StrykerNamespace.MutantControl.IsActive(0)){return!(Generator(out var x) )? """" :""test"";}else{return Generator(out var x) ? (StrykerNamespace.MutantControl.IsActive(1)?""Stryker was here!"":"""" ):(StrykerNamespace.MutantControl.IsActive(2)?"""":""test"");}}}
}";
string Value {get{if(StrykerNamespace.MutantControl.IsActive(1)){return(false?"""" :""test"");}else{if(StrykerNamespace.MutantControl.IsActive(0)){return(true?"""" :""test"");}else{return Generator(out var x) ? (StrykerNamespace.MutantControl.IsActive(2)?""Stryker was here!"":"""" ):(StrykerNamespace.MutantControl.IsActive(3)?"""":""test"");}}}}}";
ShouldMutateSourceInClassToExpected(source, expected);
}

Expand Down Expand Up @@ -1666,8 +1670,7 @@ public void ShouldNotLeakMutationsAcrossDefinitions()
}";

var expected = @"class Test {
int GetId(string input) {if(StrykerNamespace.MutantControl.IsActive(0)){return!(int.TryParse(input, out var result) )? result : 0;}else{return int.TryParse(input, out var result) ? result : 0;}}
string Value {get{if(StrykerNamespace.MutantControl.IsActive(1)){return!(Generator(out var x) )? """" :""test"";}else{return Generator(out var x) ? (StrykerNamespace.MutantControl.IsActive(2)?""Stryker was here!"":"""" ):(StrykerNamespace.MutantControl.IsActive(3)?"""":""test"");}}}}}}";
int GetId(string input) {if(StrykerNamespace.MutantControl.IsActive(1)){return(false?result :0);}else{if(StrykerNamespace.MutantControl.IsActive(0)){return(true?result :0);}else{return int.TryParse(input, out var result) ? result : 0;}}}string Value {get{if(StrykerNamespace.MutantControl.IsActive(3)){return(false?"""" :""test"");}else{if(StrykerNamespace.MutantControl.IsActive(2)){return(true?"""" :""test"");}else{return Generator(out var x) ? (StrykerNamespace.MutantControl.IsActive(4)?""Stryker was here!"":"""" ):(StrykerNamespace.MutantControl.IsActive(5)?"""":""test"");}}}}}";
ShouldMutateSourceInClassToExpected(source, expected);
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Shouldly;
using Stryker.Core.Mutators;
using System.Linq;
using Microsoft.CodeAnalysis;
using Xunit;

namespace Stryker.Core.UnitTest.Mutators
{
public class ConditionalExpressionMutatorTests : TestBase
{
[Fact]
public void ShouldBeMutationLevelStandard()
{
var target = new ConditionalExpressionMutator();
target.MutationLevel.ShouldBe(MutationLevel.Standard);
}

[Fact]
public void ShouldMutate_TwoMutations()
{
var target = new ConditionalExpressionMutator();
var source = @"251 == 73 ? 1 : 0";
var tree = CSharpSyntaxTree.ParseText(source);
var originalNode = tree.GetRoot().DescendantNodes().OfType<ConditionalExpressionSyntax>().Single();

var result = target.ApplyMutations(originalNode, null).ToList();

result.Count.ShouldBe(2, "Two mutations should have been made");
Assert.Collection(
result,
m1 => (m1.ReplacementNode is ParenthesizedExpressionSyntax pes && pes.Expression is ConditionalExpressionSyntax ces && ces.Condition.Kind() is SyntaxKind.TrueLiteralExpression).ShouldBeTrue(),
m2 => (m2.ReplacementNode is ParenthesizedExpressionSyntax pes && pes.Expression is ConditionalExpressionSyntax ces && ces.Condition.Kind() is SyntaxKind.FalseLiteralExpression).ShouldBeTrue()
);
}

[Fact]
public void ShouldMutate_DoNotTouchBranches()
{
var target = new ConditionalExpressionMutator();
var source = "251 == 73 ? 1 : 0";
var tree = CSharpSyntaxTree.ParseText(source);
var originalNode = tree.GetRoot().DescendantNodes().OfType<ConditionalExpressionSyntax>().Single();

var result = target.ApplyMutations(originalNode, null).ToList();

foreach (var mutation in result)
{
var pes = mutation.ReplacementNode.ShouldBeOfType<ParenthesizedExpressionSyntax>();
var ces = pes.Expression.ShouldBeOfType<ConditionalExpressionSyntax>();
ces.WhenTrue.IsEquivalentTo(originalNode.WhenTrue).ShouldBeTrue();
ces.WhenFalse.IsEquivalentTo(originalNode.WhenFalse).ShouldBeTrue();
}
}

[Fact]
public void ShouldNotMutateDeclarationPatterns()
{
var target = new ConditionalExpressionMutator();
var source = "var y = x is object result ? result.ToString() : null;";
SyntaxTree tree = CSharpSyntaxTree.ParseText(source);

var expressionSyntax = tree.GetRoot().DescendantNodes().OfType<ConditionalExpressionSyntax>().Single();
var result = target.ApplyMutations(expressionSyntax, null).ToList();

result.ShouldBeEmpty();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,6 @@ static void Main(string[] args)
[Theory]
[InlineData("if (Method()) => return true;")]
[InlineData("while (Method()) => age++;")]
[InlineData("(Method()? 1:2);")]
public void MutatesStatementWithMethodCallWithNoArguments(string method)
{
var target = new NegateConditionMutator();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ public void ShouldValidateExcludedMutation()

var ex = Should.Throw<InputException>(() => target.Validate<Mutator>());

ex.Message.ShouldBe($"Invalid excluded mutation (gibberish). The excluded mutations options are [Statement, Arithmetic, Block, Equality, Boolean, Logical, Assignment, Unary, Update, Checked, Linq, String, Bitwise, Initializer, Regex, NullCoalescing, Math, StringMethod]");
ex.Message.ShouldBe($"Invalid excluded mutation (gibberish). The excluded mutations options are [Statement, Arithmetic, Block, Equality, Boolean, Logical, Assignment, Unary, Update, Checked, Linq, String, Bitwise, Initializer, Regex, NullCoalescing, Math, StringMethod, Conditional]");
}

[Fact]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ private static List<IMutator> DefaultMutatorList() =>
new BinaryExpressionMutator(),
new BlockMutator(),
new BooleanMutator(),
new ConditionalExpressionMutator(),
new AssignmentExpressionMutator(),
new PrefixUnaryMutator(),
new PostfixUnaryMutator(),
Expand Down
Loading

0 comments on commit 6556ab9

Please sign in to comment.