Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implementation of mutation levels #4686

Open
wants to merge 33 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
b039851
Merge pull request #11 from stryker-mutator/master
dvcopae Oct 21, 2023
dfe19e2
Merge branch 'stryker-mutator:master' into master
dvcopae Nov 14, 2023
87c8be4
Base structure for selecting operators individually (#14)
dvcopae Nov 14, 2023
9ec5d92
#18 restrict arraydeclaration mutator (#40)
Ja4pp Nov 21, 2023
f9fbcf6
#23 restrict equalityoperator mutator (#41)
brokhiv Nov 22, 2023
e4830b4
#21 booleanliteral (#44)
Luctia Nov 24, 2023
b152138
#20 assignment operator (#46)
Luctia Nov 24, 2023
4d8db03
Add restriction for string literals (#43)
dvcopae Nov 26, 2023
5a7c317
Restrict optional chaining mutator (#45)
dvcopae Nov 26, 2023
9f9d0c7
Change mutation level specification style (#56)
dvcopae Dec 4, 2023
240614c
Read default levels v2 (#60)
dvcopae Dec 4, 2023
6b7d9a2
Restricted logical-operator-mutator.ts (#57)
brokhiv Dec 5, 2023
b91605f
#22 restrict conditionalexpression mutator (#55)
Ja4pp Dec 5, 2023
69138fa
25 restrict methodexpression mutator (#54)
brokhiv Dec 5, 2023
44378ef
29 restrict unaryoperator mutator (#53)
brokhiv Dec 5, 2023
7ac2587
#30 restrict updateoperator mutator (#51)
Ja4pp Dec 5, 2023
1523566
Added support for arrowfunction (#47)
Luctia Dec 5, 2023
ff9c4c3
#63 implement objectliteral mutator (#65)
Ja4pp Dec 6, 2023
0ad71de
Refactor mutators (#64)
dvcopae Dec 6, 2023
6629ef6
Implement mutationLevel construct BlockStatement (#66)
Ja4pp Dec 7, 2023
4043abe
implement MutationLevel construct for Regex (#67)
Ja4pp Dec 7, 2023
a66eb01
Rename mutators & enhance NodeMutatorConfiguration type (#68)
dvcopae Dec 9, 2023
c9a232f
#48 ensure code consistency between mutators tests (#75)
Ja4pp Jan 11, 2024
c0f00a8
Finish building the mutation level (#76)
dvcopae Jan 14, 2024
1ec2e3a
Provide means to calculate adjusted mutation score and implement into…
Luctia Jan 14, 2024
772c596
#72 clean up code create pr (#80)
Ja4pp Jan 16, 2024
3bb1e3b
E2e test (#82)
dvcopae Jan 16, 2024
073b2ad
#72 final touches to improve mergeability (#83)
Ja4pp Jan 16, 2024
b111450
Merge branch 'master' into master
dvcopae Jan 16, 2024
69adeba
Fix up eslintignore (#84)
dvcopae Jan 16, 2024
419d28b
Documentation (#74)
brokhiv Jan 27, 2024
905f413
Pr feedback (#85)
dvcopae Jan 27, 2024
6ef29d5
Merge branch 'stryker-mutator:master' into master
dvcopae Jan 29, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -438,12 +438,13 @@ _Note:_ It is **not** possible to combine mutation range with a [globbing expres

Default: `{}`<br />
Command line: _none_<br />
Config file: `"mutator": { "plugins": ["classProperties"], "excludedMutations": ["StringLiteral"] }`
Config file: `"mutator": { "plugins": ["classProperties"], "includedMutations": ["MutationSpecification"], "excludedMutations": ["MutationSpecification"] }`

- `plugins`: allows you to override the default [babel plugins](https://babeljs.io/docs/en/plugins) to use for JavaScript files.
By default, Stryker uses [a default list of babel plugins to parse your JS file](https://github.com/stryker-mutator/stryker-js/blob/master/packages/instrumenter/src/parsers/js-parser.ts#L8-L32). It also loads any plugins or presets you might have configured yourself with `.babelrc` or `babel.config.js` files.
In the rare situation where the plugins Stryker loads conflict with your own local plugins (for example, when using the decorators and decorators-legacy plugins together), you can override the `plugins` here to `[]`.
- `excludedMutations`: allow you to specify a [list of mutator names](https://stryker-mutator.io/docs/mutation-testing-elements/supported-mutators/#supported-mutators) to be excluded (`ignored`) from the test run. See [Disable mutants](./disable-mutants.md) for more options of how to disable specific mutants.
- `includedMutations`: allow you to specify a [list of mutator names](https://stryker-mutator.io/docs/mutation-testing-elements/supported-mutators/#supported-mutators), mutation operators, or mutation level to be included in the test run. This will exclude anything not specified in this list.
- `excludedMutations`: allow you to specify a [list of mutator names](https://stryker-mutator.io/docs/mutation-testing-elements/supported-mutators/#supported-mutators) to be excluded (`ignored`) from the test run. See [Disable mutants](./disable-mutants.md) for more options of how to disable specific mutants. In case `includedMutations` is also specified, this will exclude mutation operators from that list.

_Note: prior to Stryker version 4, the mutator also needed a `name` (or be defined as `string`). This is removed in version 4. Stryker now supports mutating of JavaScript and friend files out of the box, without the need for a mutator plugin._

Expand Down
75 changes: 75 additions & 0 deletions docs/mutation-levels.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
---
title: Mutation Levels
custom_edit_url: https://github.com/stryker-mutator/stryker-js/edit/master/docs/mutation-levels.md
---

This page describes the concept of mutation levels and how to use them in your configuration.

## Terminology
The smallest unit in mutation testing is the **mutation operator**. This is a single type of mutation, like `AdditionOperatorNegation`, which changes a `+`-operator into a `-`-operator.

Every mutation operator belongs to a **mutator**, also referred to as a **mutator group**. This is a set of mutation operators that can be applied to the same node type.
For example, the `AdditionOperatorNegation` mutation operator belongs to the `ArithmeticOperator` mutator group.

Finally, a **mutation level** is an artificial grouping of mutation operators with the purpose of striking a balance between performance and efficacy of a mutation run.
Such a level is not necessarily in line with the previously mentioned mutator groups, but designed to work right away.
Currently, mutation levels are named from `Level1` to `Level3`, where `Level1` has the best performance and `Level3` has the best efficacy.

## Specifying included/excluded mutators
By default, all of Stryker's mutators will be run on your project, which gives the maximum efficacy but also takes the most resources to run.
If you want to enable mutation levels, you can choose a level from 1 to 3, like this: ``includedMutators: ['@Level1']``.
For most users, this should suffice without further tweaks as these mutation levels are designed based upon a representable sample of JS and TS projects.

In case you want a more advanced and customized configuration, you can tweak your selected mutation level by adding or removing mutation operators and/or mutator groups.
A **mutation operator** can be specified with its literal name. **Mutator groups** and **mutation levels** are specified with the `@` prefix, for example `@ArithmeticOperator` or `@Level1`.
For example, if you want to tweak level 2 by removing all `ArithmeticOperator`'s mutation operators except for the `AdditionOperatorNegation`, you would write this:
```
includedMutators: ['@Level1', 'AdditionOperatorNegation'],
excludedMutators: ['@ArithmeticOperator']
```
When making these customization tweaks, it is recommended to test the efficacy and performance of these tweaks against the base level to see whether they make a significant enough effect.

## How to reason about modifying a mutation run

A mutation run can be modified such that either the execution time is faster, or the number of covered tests is higher. Unfortunately, these two properties cannot occur at the same time, since they are inversely proportional: as the performance of a run increases, fewer test cases will be executed, which will result in lower coverage.

### Using predefined levels

Stryker pre-defines a few mutation levels such that they provide an attractive range of efficiency-performance tradeoffs. A multitude of Javascript/TypeScript projects was used for designing these levels, and chances are that they will be suitable for your project as well. For this reason, restricting a mutation run by using a predefined level should be the first option that you should consider when you desire to gain additional performance.

### Customized configuration

However, it could be that the predefined levels do not provide suitable results for your project, and you need to further customize the configuration. Although the previous sections specify the syntax for including/excluding mutators, they do not provide the intuition on how to exactly pick the most suitable choices.

To find these best choices, we will use an external tool called [Callisto](https://github.com/stryker-mutator/callisto), which is used to quantify the resolution and the performance impact of mutation operators. This is a CLI tool that takes as input the JSON mutation report generated by Stryker and outputs a CSV file with several statistics (quality, performance impact, mutant count, etc) for each mutation operator. Callisto can be used in the following manner:


1. Generate JSON Report with Stryker

Currently, only StrykerJS generates a JSON report which is directly compatible with Callisto. To ensure that Stryker has the correct configuration for generating the JSON file, you need to make sure that the following options are selected in the configuration:

```json
{
...
"disableBail": true,
"coverageAnalysis": "perTest",
"reporters": [..., "json", ...]
...
}
```

2. (Optionally) Correct the JSON report

Some testing frameworks that StrykerJS uses might result in occasionally small mistakes, which prevents Callisto from deducing a mutation operator name. If there are any such mistakes, Callisto will report any mutants for which it cannot determine a mutation operator name through the terminal. The JSON report needs to be manually corrected with the reported errors before moving on to the next step.

3. Run Callisto on the JSON file to determine statistics for each mutation operator. For details on how to do this, please refer to the documentation of the Callisto tool.

4. Inspect the results

Using the resolution and performance impact metrics, you can make decisions about which mutator/groups to include or exclude from your project.

For example, if you would like to shorten the time a mutation run takes, take a look at the performance metric and add to the `excludedMutations` list the mutators/groups with the highest performance impact.

Similarly, you might want to execute more test cases. Then, you should look at the quality metric and add the mutators with the highest values to the `includedMutations` list. However, this metric needs to be inspected together with its mutation count; consider the scenario where an operator has a high-quality score but a small number of generated mutants. Then, the measurement is not very reliable.

Note that if you are using one of the predefined levels, some mutators might be already included or excluded.
3 changes: 2 additions & 1 deletion e2e/test/ignore-project/stryker.conf.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@
"concurrency": 2,
"coverageAnalysis": "perTest",
"mutator": {
"excludedMutations": ["ArithmeticOperator", "BlockStatement"]
"includedMutations": ["@StringLiteral", "@ConditionalExpression", "@EqualityOperator", "@LogicalOperator", "@BooleanLiteral"],
"excludedMutations": ["@ArithmeticOperator", "BlockStatementRemoval"]
},
"reporters": [
"clear-text",
Expand Down
27 changes: 25 additions & 2 deletions e2e/test/ignore-project/verify/verify.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,14 +30,37 @@ describe('After running stryker on jest-react project', () => {
});
});

it('should report mutants that result from excluded mutators with the correct ignore reason', async () => {
it('should report mutants that are excluded from the excludedMutation list with the correct ignore reason', async () => {
const report = await readMutationTestingJsonResult();
const circleResult = report.files['src/Circle.js'];
const mutantsAtLine3 = circleResult.mutants.filter(({ location }) => location.start.line === 3);
expect(mutantsAtLine3).lengthOf(2);
mutantsAtLine3.forEach((mutant) => {
expect(mutant.status).eq('Ignored');
expect(mutant.statusReason).eq('Ignored because of excluded mutation "ArithmeticOperator"');
expect(mutant.statusReason).eq('Ignored because the operator "MultiplicationOperatorNegation" is excluded from the mutation run');
});
});

it('should report mutants that are excluded because they were not in the includedMutations list', async () => {
const report = await readMutationTestingJsonResult();
const addResult = report.files['src/Add.js'];
const mutantsAtLine7 = addResult.mutants.filter(({ location }) => location.start.line === 7);
const updateOperatorMutants = mutantsAtLine7.filter(({ mutatorName }) => mutatorName === 'UpdateOperator');

const mutantsAtLine14 = addResult.mutants.filter(({ location }) => location.start.line === 14);
const unaryOperatorMutants = mutantsAtLine14.filter(({ mutatorName }) => mutatorName === 'UnaryOperator');

expect(updateOperatorMutants).lengthOf(1);
expect(unaryOperatorMutants).lengthOf(1);

updateOperatorMutants.forEach((updateMutant) => {
expect(updateMutant.status).eq('Ignored');
expect(updateMutant.statusReason).eq('Ignored because the operator "PostfixIncrementOperatorNegation" is excluded from the mutation run');
});

unaryOperatorMutants.forEach((updateMutant) => {
expect(updateMutant.status).eq('Ignored');
expect(updateMutant.statusReason).eq('Ignored because the operator "UnaryMinOperatorNegation" is excluded from the mutation run');
});
});

Expand Down
12 changes: 6 additions & 6 deletions e2e/test/ignore-project/verify/verify.js.snap
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,20 @@
exports[`After running stryker on jest-react project should report expected scores 1`] = `
Object {
"compileErrors": 0,
"ignored": 32,
"killed": 8,
"mutationScore": 50,
"ignored": 34,
"killed": 6,
"mutationScore": 42.857142857142854,
"mutationScoreBasedOnCoveredCode": 100,
"noCoverage": 8,
"pending": 0,
"runtimeErrors": 0,
"survived": 0,
"timeout": 0,
"totalCovered": 8,
"totalDetected": 8,
"totalCovered": 6,
"totalDetected": 6,
"totalInvalid": 0,
"totalMutants": 48,
"totalUndetected": 8,
"totalValid": 16,
"totalValid": 14,
}
`;
23 changes: 19 additions & 4 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading