diff --git a/docs/configuration.md b/docs/configuration.md index 854dd824b3..d773d2a894 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -176,9 +176,18 @@ Default: `['{src,lib}/**/*.js?(x)', '!{src,lib}/**/__tests__/**/*.js?(x)', '!{sr Command line: `[--mutate|-m] src/**/*.js,a.js`
Config file: `"mutate": ["src/**/*.js", "a.js"]` -With `mutate` you configure the subset of files to use for mutation testing. -Generally speaking, these should be your own source files. -This is optional, as you can choose to not mutate any files at all and perform a dry-run (running only your tests without mutating). +With `mutate` you configure the subset of files to be mutated. These should be your _production code files_, and definitely not your test files. +The default will try to guess your production code files based on sane defaults. It reads like this: + +* Include all js-like files inside the `src` or `lib` dir + * Except files inside `__tests__` directories and file names ending with `test` or `spec`. + +It is possible to specify exactly which code blocks to mutate by means of a _mutation range_. This can be done postfixing your file with `:startLine[:startColumn]-endLine[:endColumn]`. Some examples: +* `"src/app.js:1-11"` will mutate lines 1 through 11 inside app.js. +* `"src/app.js:5:4-6:4"` will mutate from line 5, column 4 through line 6 column 4 inside app.js (columns 4 are included). +* `"src/app.js:5-6:4"` will mutate from line 5, column 0 through line 6 column 4 inside app.js (column 4 is included). + +*Note:* It is not possible to combine mutation range with a globbing expression in the same line. ### `mutator` [`MutatorDescriptor`] diff --git a/e2e/test/mutation-range/jest.config.js b/e2e/test/mutation-range/jest.config.js new file mode 100644 index 0000000000..4a5b465ecb --- /dev/null +++ b/e2e/test/mutation-range/jest.config.js @@ -0,0 +1,4 @@ +module.exports = { + preset: 'ts-jest', + testEnvironment: 'node', +}; diff --git a/e2e/test/mutation-range/package-lock.json b/e2e/test/mutation-range/package-lock.json new file mode 100644 index 0000000000..6f5cef4bf1 --- /dev/null +++ b/e2e/test/mutation-range/package-lock.json @@ -0,0 +1,653 @@ +{ + "name": "specific-mutants", + "requires": true, + "lockfileVersion": 1, + "dependencies": { + "@jest/types": { + "version": "25.5.0", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-25.5.0.tgz", + "integrity": "sha512-OXD0RgQ86Tu3MazKo8bnrkDRaDXXMGUqd+kTtLtK1Zb7CRzQcaSRPPPV37SvYTdevXEBVxe0HXylEjs8ibkmCw==", + "dev": true, + "requires": { + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^1.1.1", + "@types/yargs": "^15.0.0", + "chalk": "^3.0.0" + } + }, + "@types/color-name": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@types/color-name/-/color-name-1.1.1.tgz", + "integrity": "sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ==", + "dev": true + }, + "@types/istanbul-lib-coverage": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.1.tgz", + "integrity": "sha512-hRJD2ahnnpLgsj6KWMYSrmXkM3rm2Dl1qkx6IOFD5FnuNPXJIG5L0dhgKXCYTRMGzU4n0wImQ/xfmRc4POUFlg==", + "dev": true + }, + "@types/istanbul-lib-report": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.0.tgz", + "integrity": "sha512-plGgXAPfVKFoYfa9NpYDAkseG+g6Jr294RqeqcqDixSbU34MZVJRi/P+7Y8GDpzkEwLaGZZOpKIEmeVZNtKsrg==", + "dev": true, + "requires": { + "@types/istanbul-lib-coverage": "*" + } + }, + "@types/istanbul-reports": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-1.1.1.tgz", + "integrity": "sha512-UpYjBi8xefVChsCoBpKShdxTllC9pwISirfoZsUa2AAdQg/Jd2KQGtSbw+ya7GPo7x/wAPlH6JBhKhAsXUEZNA==", + "dev": true, + "requires": { + "@types/istanbul-lib-coverage": "*", + "@types/istanbul-lib-report": "*" + } + }, + "@types/jest": { + "version": "25.2.1", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-25.2.1.tgz", + "integrity": "sha512-msra1bCaAeEdkSyA0CZ6gW1ukMIvZ5YoJkdXw/qhQdsuuDlFTcEUrUw8CLCPt2rVRUfXlClVvK2gvPs9IokZaA==", + "dev": true, + "requires": { + "jest-diff": "^25.2.1", + "pretty-format": "^25.2.1" + } + }, + "@types/node": { + "version": "14.0.27", + "resolved": "https://registry.npmjs.org/@types/node/-/node-14.0.27.tgz", + "integrity": "sha512-kVrqXhbclHNHGu9ztnAwSncIgJv/FaxmzXJvGXNdcCpV1b8u1/Mi6z6m0vwy0LzKeXFTPLH0NzwmoJ3fNCIq0g==", + "dev": true + }, + "@types/yargs": { + "version": "15.0.4", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-15.0.4.tgz", + "integrity": "sha512-9T1auFmbPZoxHz0enUFlUuKRy3it01R+hlggyVUMtnCTQRunsQYifnSGb8hET4Xo8yiC0o0r1paW3ud5+rbURg==", + "dev": true, + "requires": { + "@types/yargs-parser": "*" + } + }, + "@types/yargs-parser": { + "version": "15.0.0", + "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-15.0.0.tgz", + "integrity": "sha512-FA/BWv8t8ZWJ+gEOnLLd8ygxH/2UFbAvgEonyfN6yWGLKc7zVjbpl2Y4CTjid9h2RfgPP6SEt6uHwEOply00yw==", + "dev": true + }, + "ajv": { + "version": "6.12.2", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.2.tgz", + "integrity": "sha512-k+V+hzjm5q/Mr8ef/1Y9goCmlsK4I6Sm74teeyGvFk1XrOsbsKLjEdrvny42CZ+a8sXbk8KWpY/bDwS+FLL2UQ==", + "requires": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + } + }, + "ansi-regex": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", + "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==", + "dev": true + }, + "ansi-styles": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz", + "integrity": "sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==", + "dev": true, + "requires": { + "@types/color-name": "^1.1.1", + "color-convert": "^2.0.1" + } + }, + "asn1": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.4.tgz", + "integrity": "sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg==", + "requires": { + "safer-buffer": "~2.1.0" + } + }, + "assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=" + }, + "asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=" + }, + "aws-sign2": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", + "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=" + }, + "aws4": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.9.1.tgz", + "integrity": "sha512-wMHVg2EOHaMRxbzgFJ9gtjOOCrI80OHLG14rxi28XwOW8ux6IiEbRCGGGqCtdAIg4FQCbW20k9RsT4y3gJlFug==" + }, + "azure-storage": { + "version": "2.10.3", + "resolved": "https://registry.npmjs.org/azure-storage/-/azure-storage-2.10.3.tgz", + "integrity": "sha512-IGLs5Xj6kO8Ii90KerQrrwuJKexLgSwYC4oLWmc11mzKe7Jt2E5IVg+ZQ8K53YWZACtVTMBNO3iGuA+4ipjJxQ==", + "requires": { + "browserify-mime": "~1.2.9", + "extend": "^3.0.2", + "json-edm-parser": "0.1.2", + "md5.js": "1.3.4", + "readable-stream": "~2.0.0", + "request": "^2.86.0", + "underscore": "~1.8.3", + "uuid": "^3.0.0", + "validator": "~9.4.1", + "xml2js": "0.2.8", + "xmlbuilder": "^9.0.7" + } + }, + "bcrypt-pbkdf": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", + "integrity": "sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4=", + "requires": { + "tweetnacl": "^0.14.3" + } + }, + "browserify-mime": { + "version": "1.2.9", + "resolved": "https://registry.npmjs.org/browserify-mime/-/browserify-mime-1.2.9.tgz", + "integrity": "sha1-rrGvKN5sDXpqLOQK22j/GEIq8x8=" + }, + "caseless": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", + "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=" + }, + "chalk": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", + "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "requires": { + "delayed-stream": "~1.0.0" + } + }, + "core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" + }, + "dashdash": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", + "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=", + "requires": { + "assert-plus": "^1.0.0" + } + }, + "delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=" + }, + "diff-sequences": { + "version": "25.2.6", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-25.2.6.tgz", + "integrity": "sha512-Hq8o7+6GaZeoFjtpgvRBUknSXNeJiCx7V9Fr94ZMljNiCr9n9L8H8aJqgWOQiDDGdyn29fRNcDdRVJ5fdyihfg==", + "dev": true + }, + "ecc-jsbn": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", + "integrity": "sha1-OoOpBOVDUyh4dMVkt1SThoSamMk=", + "requires": { + "jsbn": "~0.1.0", + "safer-buffer": "^2.1.0" + } + }, + "extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" + }, + "extsprintf": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", + "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=" + }, + "fast-deep-equal": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.1.tgz", + "integrity": "sha512-8UEa58QDLauDNfpbrX55Q9jrGHThw2ZMdOky5Gl1CDtVeJDPVrG4Jxx1N8jw2gkWaff5UUuX1KJd+9zGe2B+ZA==" + }, + "fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==" + }, + "forever-agent": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", + "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=" + }, + "form-data": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz", + "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==", + "requires": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.6", + "mime-types": "^2.1.12" + } + }, + "getpass": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", + "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=", + "requires": { + "assert-plus": "^1.0.0" + } + }, + "har-schema": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", + "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=" + }, + "har-validator": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.3.tgz", + "integrity": "sha512-sNvOCzEQNr/qrvJgc3UG/kD4QtlHycrzwS+6mfTrrSq97BvaYcPZZI1ZSqGSPR73Cxn4LKTD4PttRwfU7jWq5g==", + "requires": { + "ajv": "^6.5.5", + "har-schema": "^2.0.0" + } + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "hash-base": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/hash-base/-/hash-base-3.1.0.tgz", + "integrity": "sha512-1nmYp/rhMDiE7AYkDw+lLwlAzz0AntGIe51F3RfFfEqyQ3feY2eI/NcwC6umIQVOASPMsWJLJScWKSSvzL9IVA==", + "requires": { + "inherits": "^2.0.4", + "readable-stream": "^3.6.0", + "safe-buffer": "^5.2.0" + }, + "dependencies": { + "readable-stream": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", + "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", + "requires": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + } + } + } + }, + "http-signature": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", + "integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=", + "requires": { + "assert-plus": "^1.0.0", + "jsprim": "^1.2.2", + "sshpk": "^1.7.0" + } + }, + "inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "is-typedarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", + "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=" + }, + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" + }, + "isstream": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", + "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=" + }, + "jest-diff": { + "version": "25.5.0", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-25.5.0.tgz", + "integrity": "sha512-z1kygetuPiREYdNIumRpAHY6RXiGmp70YHptjdaxTWGmA085W3iCnXNx0DhflK3vwrKmrRWyY1wUpkPMVxMK7A==", + "dev": true, + "requires": { + "chalk": "^3.0.0", + "diff-sequences": "^25.2.6", + "jest-get-type": "^25.2.6", + "pretty-format": "^25.5.0" + } + }, + "jest-get-type": { + "version": "25.2.6", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-25.2.6.tgz", + "integrity": "sha512-DxjtyzOHjObRM+sM1knti6or+eOgcGU4xVSb2HNP1TqO4ahsT+rqZg+nyqHWJSvWgKC5cG3QjGFBqxLghiF/Ig==", + "dev": true + }, + "jsbn": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", + "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=" + }, + "json-edm-parser": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/json-edm-parser/-/json-edm-parser-0.1.2.tgz", + "integrity": "sha1-HmCw/vG8CvZ7wNFG393lSGzWFbQ=", + "requires": { + "jsonparse": "~1.2.0" + } + }, + "json-schema": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz", + "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=" + }, + "json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" + }, + "json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=" + }, + "jsonparse": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/jsonparse/-/jsonparse-1.2.0.tgz", + "integrity": "sha1-XAxWhRBxYOcv50ib3eoLRMK8Z70=" + }, + "jsprim": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz", + "integrity": "sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=", + "requires": { + "assert-plus": "1.0.0", + "extsprintf": "1.3.0", + "json-schema": "0.2.3", + "verror": "1.10.0" + } + }, + "md5.js": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.4.tgz", + "integrity": "sha1-6b296UogpawYsENA/Fdk1bCdkB0=", + "requires": { + "hash-base": "^3.0.0", + "inherits": "^2.0.1" + } + }, + "mime-db": { + "version": "1.44.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.44.0.tgz", + "integrity": "sha512-/NOTfLrsPBVeH7YtFPgsVWveuL+4SjjYxaQ1xtM1KMFj7HdxlBlxeyNLzhyJVx7r4rZGJAZ/6lkKCitSc/Nmpg==" + }, + "mime-types": { + "version": "2.1.27", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.27.tgz", + "integrity": "sha512-JIhqnCasI9yD+SsmkquHBxTSEuZdQX5BuQnS2Vc7puQQQ+8yiP5AY5uWhpdv4YL4VM5c6iliiYWPgJ/nJQLp7w==", + "requires": { + "mime-db": "1.44.0" + } + }, + "oauth-sign": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", + "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==" + }, + "performance-now": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", + "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=" + }, + "pretty-format": { + "version": "25.5.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-25.5.0.tgz", + "integrity": "sha512-kbo/kq2LQ/A/is0PQwsEHM7Ca6//bGPPvU6UnsdDRSKTWxT/ru/xb88v4BJf6a69H+uTytOEsTusT9ksd/1iWQ==", + "dev": true, + "requires": { + "@jest/types": "^25.5.0", + "ansi-regex": "^5.0.0", + "ansi-styles": "^4.0.0", + "react-is": "^16.12.0" + } + }, + "process-nextick-args": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-1.0.7.tgz", + "integrity": "sha1-FQ4gt1ZZCtP5EJPyWk8q2L/zC6M=" + }, + "psl": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/psl/-/psl-1.8.0.tgz", + "integrity": "sha512-RIdOzyoavK+hA18OGGWDqUTsCLhtA7IcZ/6NCs4fFJaHBDab+pDDmDIByWFRQJq2Cd7r1OoQxBGKOaztq+hjIQ==" + }, + "punycode": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", + "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==" + }, + "qs": { + "version": "6.5.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz", + "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==" + }, + "react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "dev": true + }, + "readable-stream": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.0.6.tgz", + "integrity": "sha1-j5A0HmilPMySh4jaz80Rs265t44=", + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.1", + "isarray": "~1.0.0", + "process-nextick-args": "~1.0.6", + "string_decoder": "~0.10.x", + "util-deprecate": "~1.0.1" + }, + "dependencies": { + "string_decoder": { + "version": "0.10.31", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", + "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=" + } + } + }, + "request": { + "version": "2.88.2", + "resolved": "https://registry.npmjs.org/request/-/request-2.88.2.tgz", + "integrity": "sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw==", + "requires": { + "aws-sign2": "~0.7.0", + "aws4": "^1.8.0", + "caseless": "~0.12.0", + "combined-stream": "~1.0.6", + "extend": "~3.0.2", + "forever-agent": "~0.6.1", + "form-data": "~2.3.2", + "har-validator": "~5.1.3", + "http-signature": "~1.2.0", + "is-typedarray": "~1.0.0", + "isstream": "~0.1.2", + "json-stringify-safe": "~5.0.1", + "mime-types": "~2.1.19", + "oauth-sign": "~0.9.0", + "performance-now": "^2.1.0", + "qs": "~6.5.2", + "safe-buffer": "^5.1.2", + "tough-cookie": "~2.5.0", + "tunnel-agent": "^0.6.0", + "uuid": "^3.3.2" + } + }, + "safe-buffer": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.0.tgz", + "integrity": "sha512-fZEwUGbVl7kouZs1jCdMLdt95hdIv0ZeHg6L7qPeciMZhZ+/gdesW4wgTARkrFWEpspjEATAzUGPG8N2jJiwbg==" + }, + "safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, + "sax": { + "version": "0.5.8", + "resolved": "https://registry.npmjs.org/sax/-/sax-0.5.8.tgz", + "integrity": "sha1-1HLbIo6zMcJQaw6MFVJK25OdEsE=" + }, + "sshpk": { + "version": "1.16.1", + "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.16.1.tgz", + "integrity": "sha512-HXXqVUq7+pcKeLqqZj6mHFUMvXtOJt1uoUx09pFW6011inTMxqI8BA8PM95myrIyyKwdnzjdFjLiE6KBPVtJIg==", + "requires": { + "asn1": "~0.2.3", + "assert-plus": "^1.0.0", + "bcrypt-pbkdf": "^1.0.0", + "dashdash": "^1.12.0", + "ecc-jsbn": "~0.1.1", + "getpass": "^0.1.1", + "jsbn": "~0.1.0", + "safer-buffer": "^2.0.2", + "tweetnacl": "~0.14.0" + } + }, + "string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "requires": { + "safe-buffer": "~5.2.0" + } + }, + "supports-color": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz", + "integrity": "sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + }, + "tough-cookie": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz", + "integrity": "sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==", + "requires": { + "psl": "^1.1.28", + "punycode": "^2.1.1" + } + }, + "tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", + "requires": { + "safe-buffer": "^5.0.1" + } + }, + "tweetnacl": { + "version": "0.14.5", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", + "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=" + }, + "underscore": { + "version": "1.8.3", + "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.8.3.tgz", + "integrity": "sha1-Tz+1OxBuYJf8+ctBCfKl6b36UCI=" + }, + "uri-js": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.2.2.tgz", + "integrity": "sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ==", + "requires": { + "punycode": "^2.1.0" + } + }, + "util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" + }, + "uuid": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", + "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==" + }, + "validator": { + "version": "9.4.1", + "resolved": "https://registry.npmjs.org/validator/-/validator-9.4.1.tgz", + "integrity": "sha512-YV5KjzvRmSyJ1ee/Dm5UED0G+1L4GZnLN3w6/T+zZm8scVua4sOhYKWTUrKa0H/tMiJyO9QLHMPN+9mB/aMunA==" + }, + "verror": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", + "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=", + "requires": { + "assert-plus": "^1.0.0", + "core-util-is": "1.0.2", + "extsprintf": "^1.2.0" + } + }, + "xml2js": { + "version": "0.2.8", + "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.2.8.tgz", + "integrity": "sha1-m4FpCTFjH/CdGVdUn69U9PmAs8I=", + "requires": { + "sax": "0.5.x" + } + }, + "xmlbuilder": { + "version": "9.0.7", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-9.0.7.tgz", + "integrity": "sha1-Ey7mPS7FVlxVfiD0wi35rKaGsQ0=" + } + } +} diff --git a/e2e/test/mutation-range/package.json b/e2e/test/mutation-range/package.json new file mode 100644 index 0000000000..7f7d61de06 --- /dev/null +++ b/e2e/test/mutation-range/package.json @@ -0,0 +1,9 @@ +{ + "name": "mutation-range", + "description": "A test project for only running specific mutants, rather than all mutants", + "scripts": { + "test": "stryker run", + "test:unit": "jest", + "posttest": "mocha --require ../../tasks/ts-node-register.js verify/verify.ts" + } +} diff --git a/e2e/test/mutation-range/src/ignored.js b/e2e/test/mutation-range/src/ignored.js new file mode 100644 index 0000000000..76def76736 --- /dev/null +++ b/e2e/test/mutation-range/src/ignored.js @@ -0,0 +1,3 @@ +function ignoredFile (name) { + return 'ignored' + name; +} diff --git a/e2e/test/mutation-range/src/index.js b/e2e/test/mutation-range/src/index.js new file mode 100644 index 0000000000..fd8a88a09f --- /dev/null +++ b/e2e/test/mutation-range/src/index.js @@ -0,0 +1,7 @@ +module.exports = { + helloWorld: () => { + const hello = 'hello'; + const world = 'world'; + return `${hello} ${world}`; + }, +}; diff --git a/e2e/test/mutation-range/src/pi.js b/e2e/test/mutation-range/src/pi.js new file mode 100644 index 0000000000..71f3d75b3a --- /dev/null +++ b/e2e/test/mutation-range/src/pi.js @@ -0,0 +1 @@ +exports.radius = r => 2 * pi * r; diff --git a/e2e/test/mutation-range/stryker.conf.json b/e2e/test/mutation-range/stryker.conf.json new file mode 100644 index 0000000000..e10eb9fc29 --- /dev/null +++ b/e2e/test/mutation-range/stryker.conf.json @@ -0,0 +1,10 @@ +{ + "$schema": "../../node_modules/@stryker-mutator/core/schema/stryker-schema.json", + "mutate": ["src/index.js:3:18-3:26", "src/index.js:4:18-4:25", "src/pi.js"], + "packageManager": "npm", + "testRunner": "jest", + "tempDirName": "stryker-tmp", + "concurrency": 2, + "coverageAnalysis": "off", + "reporters": ["event-recorder", "progress", "html"] +} diff --git a/e2e/test/mutation-range/test/index.spec.js b/e2e/test/mutation-range/test/index.spec.js new file mode 100644 index 0000000000..8dbc64435b --- /dev/null +++ b/e2e/test/mutation-range/test/index.spec.js @@ -0,0 +1,7 @@ +const helloWorld = require('../src/index').helloWorld; + +describe('Hello World', () => { + it('should output hello world', () => { + expect(helloWorld()).toBe('hello world'); + }); +}); diff --git a/e2e/test/mutation-range/verify/verify.ts b/e2e/test/mutation-range/verify/verify.ts new file mode 100644 index 0000000000..d0ac28089c --- /dev/null +++ b/e2e/test/mutation-range/verify/verify.ts @@ -0,0 +1,16 @@ +import { expectMetrics } from '../../../helpers'; + +describe('Verify stryker has ran correctly', () => { + + it('should report correct score', async () => { + await expectMetrics({ + ignored: 0, + killed: 2, + mutationScore: 40, + noCoverage: 0, + survived: 3, + timeout: 0, + compileErrors: 0 + }); + }); +}); diff --git a/packages/api/src/core/index.ts b/packages/api/src/core/index.ts index 4ef67d5724..a712579971 100644 --- a/packages/api/src/core/index.ts +++ b/packages/api/src/core/index.ts @@ -9,3 +9,4 @@ export * from './stryker-options-schema'; export * from './partial-stryker-options'; export * from './instrument'; export * from './mutant-coverage'; +export * from './mutation-range'; diff --git a/packages/api/src/core/mutation-range.ts b/packages/api/src/core/mutation-range.ts new file mode 100644 index 0000000000..326f73e22a --- /dev/null +++ b/packages/api/src/core/mutation-range.ts @@ -0,0 +1,25 @@ +/** + * Represents a range of mutants that the instrumenter should instrument + */ +export interface MutationRange { + /** + * The filename of the file that this range belongs to + */ + fileName: string; + + /** + * The start of the range to instrument, by line and column number, inclusive + */ + start: { + line: number; + column: number; + }; + + /** + * The end of the range to instrument, by line and number, inclusive + */ + end: { + line: number; + column: number; + }; +} diff --git a/packages/core/src/config/options-validator.ts b/packages/core/src/config/options-validator.ts index 84a88f3413..70529d4a1c 100644 --- a/packages/core/src/config/options-validator.ts +++ b/packages/core/src/config/options-validator.ts @@ -1,5 +1,7 @@ import os from 'os'; +import { hasMagic } from 'glob'; + import Ajv, { ValidateFunction } from 'ajv'; import { StrykerOptions, strykerCoreSchema } from '@stryker-mutator/api/core'; import { tokens, commonTokens } from '@stryker-mutator/api/plugin'; @@ -11,6 +13,7 @@ import { coreTokens } from '../di'; import { ConfigError } from '../errors'; import { isWarningEnabled } from '../utils/object-utils'; import { CommandTestRunner } from '../test-runner/command-test-runner'; +import { MUTATION_RANGE_REGEX } from '../input'; import { describeErrors } from './validation-errors'; @@ -85,6 +88,31 @@ export class OptionsValidator { 'Using "testRunnerNodeArgs" together with the "command" test runner is not supported, these arguments will be ignored. You can add your custom arguments by setting the "commandRunner.command" option.' ); } + options.mutate.forEach((mutateString, index) => { + const match = MUTATION_RANGE_REGEX.exec(mutateString); + if (match) { + if (hasMagic(mutateString)) { + additionalErrors.push( + `Config option "mutate[${index}]" is invalid. Cannot combine a glob expression with a mutation range in "${mutateString}".` + ); + } else { + const [_, _fileName, mutationRange, startLine, _startColumn, endLine, _endColumn] = match; + const start = parseInt(startLine, 10); + const end = parseInt(endLine, 10); + if (start < 1) { + additionalErrors.push( + `Config option "mutate[${index}]" is invalid. Mutation range "${mutationRange}" is invalid, line ${start} does not exist (lines start at 1).` + ); + } + if (start > end) { + additionalErrors.push( + `Config option "mutate[${index}]" is invalid. Mutation range "${mutationRange}" is invalid. The "from" line number (${start}) should be less then the "to" line number (${end}).` + ); + } + } + } + }); + additionalErrors.forEach((error) => this.log.error(error)); this.throwErrorIfNeeded(additionalErrors); } diff --git a/packages/core/src/input/index.ts b/packages/core/src/input/index.ts new file mode 100644 index 0000000000..b5f3546284 --- /dev/null +++ b/packages/core/src/input/index.ts @@ -0,0 +1,2 @@ +export * from './input-file-collection'; +export * from './input-file-resolver'; diff --git a/packages/core/src/input/input-file-collection.ts b/packages/core/src/input/input-file-collection.ts index 841500ff0a..e36efc763c 100644 --- a/packages/core/src/input/input-file-collection.ts +++ b/packages/core/src/input/input-file-collection.ts @@ -1,16 +1,18 @@ import os from 'os'; -import { File } from '@stryker-mutator/api/core'; +import { File, MutationRange } from '@stryker-mutator/api/core'; import { Logger } from '@stryker-mutator/api/logging'; import { normalizeWhitespaces } from '@stryker-mutator/util'; export class InputFileCollection { public readonly files: readonly File[]; public readonly filesToMutate: readonly File[]; + public readonly mutationRanges: readonly MutationRange[]; - constructor(files: readonly File[], mutateGlobResult: readonly string[]) { + constructor(files: readonly File[], mutateGlobResult: readonly string[], mutationRangeToInstrument: readonly MutationRange[]) { this.files = files; this.filesToMutate = files.filter((file) => mutateGlobResult.some((name) => name === file.name)); + this.mutationRanges = mutationRangeToInstrument; } public logFiles(log: Logger): void { diff --git a/packages/core/src/input/input-file-resolver.ts b/packages/core/src/input/input-file-resolver.ts index 092ee7494b..3d97e4712f 100644 --- a/packages/core/src/input/input-file-resolver.ts +++ b/packages/core/src/input/input-file-resolver.ts @@ -4,7 +4,7 @@ import fs from 'fs'; import { from } from 'rxjs'; import { filter, map, mergeMap, toArray } from 'rxjs/operators'; -import { File, StrykerOptions } from '@stryker-mutator/api/core'; +import { File, StrykerOptions, MutationRange } from '@stryker-mutator/api/core'; import { Logger } from '@stryker-mutator/api/logging'; import { commonTokens, tokens } from '@stryker-mutator/api/plugin'; import { SourceFile } from '@stryker-mutator/api/report'; @@ -26,6 +26,8 @@ function toReportSourceFile(file: File): SourceFile { const IGNORE_PATTERN_CHARACTER = '!'; +export const MUTATION_RANGE_REGEX = /(.*?):((\d+)(?::(\d+))?-(\d+)(?::(\d+))?)$/; + /** * When characters are represented as the octal values of its utf8 encoding * e.g. å becomes \303\245 in git.exe output @@ -64,9 +66,13 @@ export class InputFileResolver { } public async resolve(): Promise { - const [inputFileNames, mutateFiles] = await Promise.all([this.resolveInputFiles(), this.resolveMutateFiles()]); + const [inputFileNames, mutateFiles, mutationRange] = await Promise.all([ + this.resolveInputFiles(), + this.resolveMutateFiles(), + this.resolveMutationRange(), + ]); const files: File[] = await this.readFiles(inputFileNames); - const inputFileCollection = new InputFileCollection(files, mutateFiles); + const inputFileCollection = new InputFileCollection(files, mutateFiles, mutationRange); this.reportAllSourceFilesRead(files); inputFileCollection.logFiles(this.log); return inputFileCollection; @@ -97,6 +103,19 @@ export class InputFileResolver { } } + private resolveMutationRange(): MutationRange[] { + return this.mutatePatterns + .map((fileToMutate) => MUTATION_RANGE_REGEX.exec(fileToMutate)) + .filter(notEmpty) + .map(([_, fileName, _mutationRange, startLine, startColumn = '0', endLine, endColumn = Number.MAX_SAFE_INTEGER.toString()]) => { + return { + fileName: path.resolve(fileName), + start: { line: parseInt(startLine) - 1, column: parseInt(startColumn) }, + end: { line: parseInt(endLine) - 1, column: parseInt(endColumn) }, + }; + }); + } + /** * Takes a list of globbing patterns and expands them into files. * If a patterns starts with a `!`, it negates the pattern. @@ -117,6 +136,10 @@ export class InputFileResolver { } private async expandPattern(globbingExpression: string, logAboutUselessPatterns: boolean): Promise { + if (MUTATION_RANGE_REGEX.exec(globbingExpression)) { + globbingExpression = globbingExpression.replace(MUTATION_RANGE_REGEX, '$1'); + } + const fileNames = (await glob(globbingExpression)).map((relativeFile) => path.resolve(relativeFile)); if (!fileNames.length && logAboutUselessPatterns) { this.log.warn(`Globbing expression "${globbingExpression}" did not result in any files.`); diff --git a/packages/core/src/process/1-prepare-executor.ts b/packages/core/src/process/1-prepare-executor.ts index e5ff696555..83d7637136 100644 --- a/packages/core/src/process/1-prepare-executor.ts +++ b/packages/core/src/process/1-prepare-executor.ts @@ -3,7 +3,7 @@ import { commonTokens, Injector, tokens } from '@stryker-mutator/api/plugin'; import { LogConfigurator } from '../logging'; import { buildMainInjector, coreTokens, CliOptionsProvider } from '../di'; -import { InputFileResolver } from '../input/input-file-resolver'; +import { InputFileResolver } from '../input'; import { ConfigError } from '../errors'; import { MutantInstrumenterContext } from '.'; diff --git a/packages/core/src/process/2-mutant-instrumenter-executor.ts b/packages/core/src/process/2-mutant-instrumenter-executor.ts index 3b454473b5..c132db4ed2 100644 --- a/packages/core/src/process/2-mutant-instrumenter-executor.ts +++ b/packages/core/src/process/2-mutant-instrumenter-executor.ts @@ -3,7 +3,7 @@ import { Instrumenter, InstrumentResult } from '@stryker-mutator/instrumenter'; import { File, StrykerOptions } from '@stryker-mutator/api/core'; import { MainContext, coreTokens } from '../di'; -import { InputFileCollection } from '../input/input-file-collection'; +import { InputFileCollection } from '../input'; import { Sandbox } from '../sandbox/sandbox'; import { LoggingClientContext } from '../logging'; @@ -31,7 +31,10 @@ export class MutantInstrumenterExecutor { const instrumenter = this.injector.injectClass(Instrumenter); // Instrument files in-memory - const instrumentResult = await instrumenter.instrument(this.inputFiles.filesToMutate, this.options.mutator); + const instrumentResult = await instrumenter.instrument(this.inputFiles.filesToMutate, { + ...this.options.mutator, + mutationRanges: this.inputFiles.mutationRanges, + }); // Preprocess sandbox files const preprocess = this.injector.injectFunction(createPreprocessor); diff --git a/packages/core/src/reporters/mutation-test-report-helper.ts b/packages/core/src/reporters/mutation-test-report-helper.ts index f66d1a068d..38bfd96023 100644 --- a/packages/core/src/reporters/mutation-test-report-helper.ts +++ b/packages/core/src/reporters/mutation-test-report-helper.ts @@ -21,7 +21,7 @@ import { CompleteDryRunResult, MutantRunResult, MutantRunStatus } from '@stryker import { CheckStatus, PassedCheckResult, CheckResult } from '@stryker-mutator/api/check'; import { coreTokens } from '../di'; -import { InputFileCollection } from '../input/input-file-collection'; +import { InputFileCollection } from '../input'; import { setExitCode } from '../utils/object-utils'; import { MutantTestCoverage } from '../mutants/find-mutant-test-coverage'; import { mutatedLines, originalLines } from '../utils/mutant-utils'; diff --git a/packages/core/src/stryker-cli.ts b/packages/core/src/stryker-cli.ts index 109c3a332b..cac806d3c8 100644 --- a/packages/core/src/stryker-cli.ts +++ b/packages/core/src/stryker-cli.ts @@ -67,7 +67,7 @@ export class StrykerCli { ) .option( '-m, --mutate ', - 'A comma separated list of globbing expression used for selecting the files that should be mutated. Example: src/**/*.js,a.js', + 'A comma separated list of globbing expression used for selecting the files that should be mutated. Example: src/**/*.js,a.js. You can also specify specific lines and columns to mutate by adding :startLine[:startColumn]-endLine[:endColumn]. This will execute all mutants inside that range. It cannot be combined with glob patterns. Example: src/index.js:1:3-1:5', list ) .option( diff --git a/packages/core/test/integration/input/input-file-resolver.it.spec.ts b/packages/core/test/integration/input/input-file-resolver.it.spec.ts index b6fe430b62..c70e9c06b6 100644 --- a/packages/core/test/integration/input/input-file-resolver.it.spec.ts +++ b/packages/core/test/integration/input/input-file-resolver.it.spec.ts @@ -2,7 +2,7 @@ import { factory, testInjector } from '@stryker-mutator/test-helpers'; import { expect } from 'chai'; import { coreTokens } from '../../../src/di'; -import { InputFileResolver } from '../../../src/input/input-file-resolver'; +import { InputFileResolver } from '../../../src/input'; import { resolveFromRoot } from '../../helpers/test-utils'; const resolveTestResource = resolveFromRoot.bind(undefined, 'testResources', 'input-files'); diff --git a/packages/core/test/unit/config/options-validator.spec.ts b/packages/core/test/unit/config/options-validator.spec.ts index f7000d2091..d4e2b454c2 100644 --- a/packages/core/test/unit/config/options-validator.spec.ts +++ b/packages/core/test/unit/config/options-validator.spec.ts @@ -185,6 +185,35 @@ describe(OptionsValidator.name, () => { 'DEPRECATED. Use of "mutator" as string is no longer needed. You can remove it from your configuration. Stryker now supports mutating of JavaScript and friend files out of the box.' ); }); + + it('should accept mutationRange without a glob pattern', () => { + testInjector.options.mutate = ['src/index.ts:1:0-2:0']; + actAssertValid(); + }); + + it('should not accept mutationRange for line < 1 (lines are 1 based)', () => { + testInjector.options.mutate = ['src/app.ts:5:0-6:0', 'src/index.ts:0:0-2:0']; + actValidationErrors('Config option "mutate[1]" is invalid. Mutation range "0:0-2:0" is invalid, line 0 does not exist (lines start at 1).'); + }); + + it('should not accept mutationRange for start > end', () => { + testInjector.options.mutate = ['src/index.ts:6-5']; + actValidationErrors( + 'Config option "mutate[0]" is invalid. Mutation range "6-5" is invalid. The "from" line number (6) should be less then the "to" line number (5).' + ); + }); + + it('should not accept mutationRange with a glob pattern', () => { + testInjector.options.mutate = ['src/index.*.ts:1:0-2:0']; + actValidationErrors( + 'Config option "mutate[0]" is invalid. Cannot combine a glob expression with a mutation range in "src/index.*.ts:1:0-2:0".' + ); + }); + + it('should not accept mutationRange (with no column numbers) with a glob pattern', () => { + testInjector.options.mutate = ['src/index.*.ts:1-2']; + actValidationErrors('Config option "mutate[0]" is invalid. Cannot combine a glob expression with a mutation range in "src/index.*.ts:1-2".'); + }); }); describe('testFramework', () => { diff --git a/packages/core/test/unit/input/input-file-resolver.spec.ts b/packages/core/test/unit/input/input-file-resolver.spec.ts index cd74543a0a..28ba562188 100644 --- a/packages/core/test/unit/input/input-file-resolver.spec.ts +++ b/packages/core/test/unit/input/input-file-resolver.spec.ts @@ -2,7 +2,7 @@ import os from 'os'; import path from 'path'; import fs from 'fs'; -import { File } from '@stryker-mutator/api/core'; +import { File, MutationRange } from '@stryker-mutator/api/core'; import { SourceFile } from '@stryker-mutator/api/report'; import { testInjector, factory, assertions, tick } from '@stryker-mutator/test-helpers'; import { childProcessAsPromised, errorToString, Task } from '@stryker-mutator/util'; @@ -10,7 +10,7 @@ import { expect } from 'chai'; import sinon from 'sinon'; import { coreTokens } from '../../../src/di'; -import { InputFileResolver } from '../../../src/input/input-file-resolver'; +import { InputFileResolver } from '../../../src/input'; import { BroadcastReporter } from '../../../src/reporters/broadcast-reporter'; import * as fileUtils from '../../../src/utils/file-utils'; import { Mock, mock } from '../../helpers/producers'; @@ -189,6 +189,46 @@ describe(InputFileResolver.name, () => { await onGoingWork; }); + describe('with mutation range definitions', () => { + it('should remove specific mutant descriptors when matching with line and column', async () => { + testInjector.options.mutate = ['mute1:1:2-2:2']; + testInjector.options.files = ['file1', 'mute1', 'file2', 'mute2', 'file3']; + sut = createSut(); + const result = await sut.resolve(); + expect(result.filesToMutate.map((_) => _.name)).to.deep.equal([path.resolve('/mute1.js')]); + }); + + it('should parse the mutation range', async () => { + testInjector.options.mutate = ['mute1:1:2-2:2']; + testInjector.options.files = ['file1', 'mute1', 'file2', 'mute2', 'file3']; + sut = createSut(); + const result = await sut.resolve(); + const expectedRanges: MutationRange[] = [ + { + start: { + column: 2, + line: 0, // internally, Stryker works 0-based + }, + end: { + column: 2, + line: 1, + }, + fileName: path.resolve('mute1'), + }, + ]; + expect(result.mutationRanges).deep.eq(expectedRanges); + }); + + it('should default column numbers if not present', async () => { + testInjector.options.mutate = ['mute1:6-12']; + testInjector.options.files = ['file1', 'mute1', 'file2', 'mute2', 'file3']; + sut = createSut(); + const result = await sut.resolve(); + expect(result.mutationRanges[0].start).deep.eq({ column: 0, line: 5 }); + expect(result.mutationRanges[0].end).deep.eq({ column: Number.MAX_SAFE_INTEGER, line: 11 }); + }); + }); + describe('with mutate file expressions', () => { it('should result in the expected mutate files', async () => { testInjector.options.mutate = ['mute*']; diff --git a/packages/core/test/unit/process/1-prepare-executor.spec.ts b/packages/core/test/unit/process/1-prepare-executor.spec.ts index f958caac25..d9ad286ac0 100644 --- a/packages/core/test/unit/process/1-prepare-executor.spec.ts +++ b/packages/core/test/unit/process/1-prepare-executor.spec.ts @@ -11,8 +11,7 @@ import { coreTokens } from '../../../src/di'; import { LogConfigurator, LoggingClientContext } from '../../../src/logging'; import * as buildMainInjectorModule from '../../../src/di/build-main-injector'; import { Timer } from '../../../src/utils/timer'; -import { InputFileResolver } from '../../../src/input/input-file-resolver'; -import { InputFileCollection } from '../../../src/input/input-file-collection'; +import { InputFileResolver, InputFileCollection } from '../../../src/input'; import { TemporaryDirectory } from '../../../src/utils/temporary-directory'; import { ConfigError } from '../../../src/errors'; @@ -29,7 +28,7 @@ describe(PrepareExecutor.name, () => { let sut: PrepareExecutor; beforeEach(() => { - inputFiles = new InputFileCollection([new File('index.js', 'console.log("hello world");')], ['index.js']); + inputFiles = new InputFileCollection([new File('index.js', 'console.log("hello world");')], ['index.js'], []); cliOptions = {}; timerMock = sinon.createStubInstance(Timer); temporaryDirectoryMock = sinon.createStubInstance(TemporaryDirectory); @@ -93,12 +92,12 @@ describe(PrepareExecutor.name, () => { }); it('should reject when no input files where found', async () => { - inputFileResolverMock.resolve.resolves(new InputFileCollection([], [])); + inputFileResolverMock.resolve.resolves(new InputFileCollection([], [], [])); await expect(sut.execute()).rejectedWith(ConfigError, 'No input files found'); }); it('should not create the temp directory when no input files where found', async () => { - inputFileResolverMock.resolve.resolves(new InputFileCollection([], [])); + inputFileResolverMock.resolve.resolves(new InputFileCollection([], [], [])); await expect(sut.execute()).rejected; expect(temporaryDirectoryMock.initialize).not.called; }); diff --git a/packages/core/test/unit/process/2-mutant-instrumenter-executor.spec.ts b/packages/core/test/unit/process/2-mutant-instrumenter-executor.spec.ts index db4c4cfcac..0f411186f6 100644 --- a/packages/core/test/unit/process/2-mutant-instrumenter-executor.spec.ts +++ b/packages/core/test/unit/process/2-mutant-instrumenter-executor.spec.ts @@ -9,7 +9,7 @@ import { Checker } from '@stryker-mutator/api/check'; import { I } from '@stryker-mutator/util'; import { DryRunContext, MutantInstrumenterContext, MutantInstrumenterExecutor } from '../../../src/process'; -import { InputFileCollection } from '../../../src/input/input-file-collection'; +import { InputFileCollection } from '../../../src/input'; import { coreTokens } from '../../../src/di'; import { createConcurrencyTokenProviderMock, createCheckerPoolMock, ConcurrencyTokenProviderMock } from '../../helpers/producers'; import { createCheckerFactory } from '../../../src/checker/checker-facade'; @@ -47,7 +47,7 @@ describe(MutantInstrumenterExecutor.name, () => { preprocess: sinon.stub(), }; sandboxFilePreprocessorMock.preprocess.resolves([mutatedFile, testFile]); - inputFiles = new InputFileCollection([originalFile, testFile], [mutatedFile.name]); + inputFiles = new InputFileCollection([originalFile, testFile], [mutatedFile.name], []); injectorMock = factory.injector(); sut = new MutantInstrumenterExecutor(injectorMock as Injector, inputFiles, testInjector.options); injectorMock.injectClass.withArgs(Instrumenter).returns(instrumenterMock); @@ -65,7 +65,7 @@ describe(MutantInstrumenterExecutor.name, () => { testInjector.options.mutator.plugins = ['functionSent']; testInjector.options.mutator.excludedMutations = ['fooMutator']; await sut.execute(); - const expectedInstrumenterOptions: InstrumenterOptions = testInjector.options.mutator; + const expectedInstrumenterOptions: InstrumenterOptions = { ...testInjector.options.mutator, mutationRanges: [] }; expect(instrumenterMock.instrument).calledOnceWithExactly([originalFile], expectedInstrumenterOptions); }); diff --git a/packages/core/test/unit/reporters/mutation-test-report-helper.spec.ts b/packages/core/test/unit/reporters/mutation-test-report-helper.spec.ts index 6069568ec9..0759a1d2b5 100644 --- a/packages/core/test/unit/reporters/mutation-test-report-helper.spec.ts +++ b/packages/core/test/unit/reporters/mutation-test-report-helper.spec.ts @@ -17,7 +17,7 @@ import { CompleteDryRunResult } from '@stryker-mutator/api/test-runner'; import { CheckStatus } from '@stryker-mutator/api/check'; import { coreTokens } from '../../../src/di'; -import { InputFileCollection } from '../../../src/input/input-file-collection'; +import { InputFileCollection } from '../../../src/input'; import { MutationTestReportHelper } from '../../../src/reporters/mutation-test-report-helper'; import * as objectUtils from '../../../src/utils/object-utils'; import { createMutantTestCoverage } from '../../helpers/producers'; @@ -38,6 +38,7 @@ describe(MutationTestReportHelper.name, () => { filesToMutate: [], // eslint-disable-next-line @typescript-eslint/no-empty-function logFiles: () => {}, + mutationRanges: [], }; dryRunResult = factory.completeDryRunResult(); }); @@ -258,7 +259,7 @@ describe(MutationTestReportHelper.name, () => { describe('reportOne', () => { beforeEach(() => { - inputFiles = new InputFileCollection([new File('add.js', 'function add(a, b) {\n return a + b;\n}\n')], ['add.js']); + inputFiles = new InputFileCollection([new File('add.js', 'function add(a, b) {\n return a + b;\n}\n')], ['add.js'], []); }); it('should map simple attributes to the mutant result', () => { diff --git a/packages/instrumenter/src/instrumenter.ts b/packages/instrumenter/src/instrumenter.ts index 9c4d6de779..bfb03fa030 100644 --- a/packages/instrumenter/src/instrumenter.ts +++ b/packages/instrumenter/src/instrumenter.ts @@ -2,7 +2,7 @@ import path from 'path'; import { tokens, commonTokens } from '@stryker-mutator/api/plugin'; import { Logger } from '@stryker-mutator/api/logging'; -import { File } from '@stryker-mutator/api/core'; +import { File, MutationRange } from '@stryker-mutator/api/core'; import { createParser } from './parsers'; import { transform, MutantCollector } from './transformers'; @@ -30,7 +30,7 @@ export class Instrumenter { const parse = createParser(options); for await (const file of files) { const ast = await parse(file.textContent, file.name); - transform(ast, mutantCollector, { options }); + transform(ast, mutantCollector, { options: { ...options, mutationRanges: options.mutationRanges.map(toBabelLineNumber) } }); const mutatedContent = print(ast); outFiles.push(new File(file.name, mutatedContent)); if (this.logger.isDebugEnabled()) { @@ -47,3 +47,17 @@ export class Instrumenter { }; } } + +function toBabelLineNumber(range: MutationRange): MutationRange { + return { + ...range, + end: { + ...range.end, + line: range.end.line + 1, + }, + start: { + ...range.start, + line: range.start.line + 1, + }, + }; +} diff --git a/packages/instrumenter/src/transformers/babel-transformer.ts b/packages/instrumenter/src/transformers/babel-transformer.ts index e4d4e43175..90c67bb221 100644 --- a/packages/instrumenter/src/transformers/babel-transformer.ts +++ b/packages/instrumenter/src/transformers/babel-transformer.ts @@ -7,7 +7,7 @@ import { File } from '@babel/core'; import { placeMutants } from '../mutant-placers'; import { mutate } from '../mutators'; -import { instrumentationBabelHeader, isTypeNode, isImportDeclaration } from '../util/syntax-helpers'; +import { instrumentationBabelHeader, isTypeNode, isImportDeclaration, locationIncluded, locationOverlaps } from '../util/syntax-helpers'; import { AstFormat } from '../syntax'; import { AstTransformer } from '.'; @@ -20,12 +20,20 @@ export const transformBabel: AstTransformer = ( // Wrap the AST in a `new File`, so `nodePath.buildCodeFrameError` works // https://github.com/babel/babel/issues/11889 const file = new File({ filename: originFileName }, { code: rawContent, ast: root }); + + // Range filters that are in scope for the current file + const mutantRangesForCurrentFile = options.mutationRanges.filter((mutantRange) => mutantRange.fileName === originFileName); traverse(file.ast, { enter(path) { - if (isTypeNode(path) || isImportDeclaration(path) || path.isDecorator()) { - // Don't mutate type declarations or import statements + // Don't mutate import statements, type definitions and nodes that don't have overlap with the current range filter + if ( + isTypeNode(path) || + isImportDeclaration(path) || + path.isDecorator() || + (mutantRangesForCurrentFile.length && mutantRangesForCurrentFile.every((range) => !locationOverlaps(range, path.node.loc!))) + ) { path.skip(); - } else { + } else if (!mutantRangesForCurrentFile.length || mutantRangesForCurrentFile.some((range) => locationIncluded(range, path.node.loc!))) { mutate(path, options).forEach((mutant) => { mutantCollector.add(originFileName, mutant, offset?.position, offset?.line); }); diff --git a/packages/instrumenter/src/transformers/transformer-options.ts b/packages/instrumenter/src/transformers/transformer-options.ts index a216f603ae..67d1e214b4 100644 --- a/packages/instrumenter/src/transformers/transformer-options.ts +++ b/packages/instrumenter/src/transformers/transformer-options.ts @@ -1,3 +1,7 @@ +import { MutationRange } from '@stryker-mutator/api/core'; + import { MutatorOptions } from '../mutators'; -export type TransformerOptions = MutatorOptions; +export interface TransformerOptions extends MutatorOptions { + mutationRanges: readonly MutationRange[]; +} diff --git a/packages/instrumenter/src/util/syntax-helpers.ts b/packages/instrumenter/src/util/syntax-helpers.ts index ec24517f9b..e909d24ecf 100644 --- a/packages/instrumenter/src/util/syntax-helpers.ts +++ b/packages/instrumenter/src/util/syntax-helpers.ts @@ -201,3 +201,24 @@ const flowTypeAnnotationNodeTypes: ReadonlyArray = Object.fr export function isImportDeclaration(path: NodePath): boolean { return types.isTSImportEqualsDeclaration(path.node) || path.isImportDeclaration(); } + +/** + * Determines if a location (needle) is included in an other location (haystack) + * @param haystack The range to look in + * @param needle the range to search for + */ +export function locationIncluded(haystack: types.SourceLocation, needle: types.SourceLocation): boolean { + const startIncluded = + haystack.start.line < needle.start.line || (haystack.start.line === needle.start.line && haystack.start.column <= needle.start.column); + const endIncluded = haystack.end.line > needle.end.line || (haystack.end.line === needle.end.line && haystack.end.column >= needle.end.column); + return startIncluded && endIncluded; +} + +/** + * Determines if two locations overlap with each other + */ +export function locationOverlaps(a: types.SourceLocation, b: types.SourceLocation): boolean { + const startIncluded = a.start.line < b.end.line || (a.start.line === b.end.line && a.start.column <= b.end.column); + const endIncluded = a.end.line > b.start.line || (a.end.line === b.start.line && a.end.column >= b.start.column); + return startIncluded && endIncluded; +} diff --git a/packages/instrumenter/test/helpers/factories.ts b/packages/instrumenter/test/helpers/factories.ts index 4e4f2c8ae7..b4edfd846c 100644 --- a/packages/instrumenter/test/helpers/factories.ts +++ b/packages/instrumenter/test/helpers/factories.ts @@ -18,6 +18,7 @@ export function createParserOptions(overrides?: Partial): ParserO export function createTransformerOptions(overrides?: Partial): TransformerOptions { return { excludedMutations: [], + mutationRanges: [], ...overrides, }; } diff --git a/packages/instrumenter/test/integration/instrumenter.it.spec.ts b/packages/instrumenter/test/integration/instrumenter.it.spec.ts index f719d7a337..077f0ffd79 100644 --- a/packages/instrumenter/test/integration/instrumenter.it.spec.ts +++ b/packages/instrumenter/test/integration/instrumenter.it.spec.ts @@ -61,6 +61,53 @@ describe('instrumenter integration', () => { }); }); + describe('with mutation ranges', () => { + it('should only mutate specific mutants for the given file', async () => { + const fullFileName = resolveTestResource('instrumenter', 'specific-mutants.ts'); + + await arrangeAndActAssert('specific-mutants.ts', { + ...createInstrumenterOptions(), + mutationRanges: [ + { + fileName: fullFileName, + start: { line: 0, column: 10 }, + end: { line: 0, column: 15 }, + }, + { + fileName: fullFileName, + start: { line: 3, column: 4 }, + end: { line: 3, column: 11 }, + }, + { + fileName: fullFileName, + start: { line: 7, column: 15 }, + end: { line: 7, column: 22 }, + }, + { + fileName: fullFileName, + start: { line: 18, column: 2 }, + end: { line: 19, column: 75 }, + }, + ], + }); + }); + + it('should not make any mutations in a file not found in the specific mutants', async () => { + const fullFileName = resolveTestResource('instrumenter', 'specific-mutants.ts'); + + await arrangeAndActAssert('specific-no-mutants.ts', { + ...createInstrumenterOptions(), + mutationRanges: [ + { + fileName: fullFileName, + start: { line: 1, column: 10 }, + end: { line: 1, column: 15 }, + }, + ], + }); + }); + }); + async function arrangeAndActAssert(fileName: string, options = createInstrumenterOptions()) { const fullFileName = resolveTestResource('instrumenter', fileName); const file = new File(fullFileName, await fsPromises.readFile(fullFileName)); diff --git a/packages/instrumenter/test/unit/instrumenter.spec.ts b/packages/instrumenter/test/unit/instrumenter.spec.ts index 9c7e356614..63b158e5a0 100644 --- a/packages/instrumenter/test/unit/instrumenter.spec.ts +++ b/packages/instrumenter/test/unit/instrumenter.spec.ts @@ -37,6 +37,25 @@ describe(Instrumenter.name, () => { expect(actualResult.files).deep.eq([new File('foo.js', output[0]), new File('bar.ts', output[1])]); }); + it('should convert line numbers to be 1-based (for babel internals)', async () => { + // Arrange + const { input } = arrangeTwoFiles(); + + // Act + await sut.instrument( + input, + createInstrumenterOptions({ mutationRanges: [{ fileName: 'foo.js', start: { line: 0, column: 0 }, end: { line: 6, column: 42 } }] }) + ); + + // Assert + const actual = helper.transformerStub.getCall(0).args[2]; + const expected: transformers.TransformerOptions = createInstrumenterOptions({ + excludedMutations: [], + mutationRanges: [{ fileName: 'foo.js', start: { line: 1, column: 0 }, end: { line: 7, column: 42 } }], + }); + expect(actual).deep.eq({ options: expected }); + }); + it('should log about instrumenting', async () => { await sut.instrument([new File('b.js', 'foo'), new File('a.js', 'bar')], createInstrumenterOptions()); expect(testInjector.logger.debug).calledWith('Instrumenting %d source files with mutants', 2); diff --git a/packages/instrumenter/test/unit/transformers/babel-transformer.spec.ts b/packages/instrumenter/test/unit/transformers/babel-transformer.spec.ts index 40bad8901d..2e4f46befc 100644 --- a/packages/instrumenter/test/unit/transformers/babel-transformer.spec.ts +++ b/packages/instrumenter/test/unit/transformers/babel-transformer.spec.ts @@ -3,6 +3,8 @@ import { expect } from 'chai'; import { types, NodePath } from '@babel/core'; import generate from '@babel/generator'; +import { normalizeWhitespaces } from '@stryker-mutator/util'; + import { transformerContextStub } from '../../helpers/stubs'; import { TransformerContext } from '../../../src/transformers'; import * as mutators from '../../../src/mutators'; @@ -11,6 +13,8 @@ import { MutantCollector } from '../../../src/transformers/mutant-collector'; import { transformBabel } from '../../../src/transformers/babel-transformer'; import { createJSAst, createNamedNodeMutation, createMutant, createTSAst } from '../../helpers/factories'; import { instrumentationBabelHeader } from '../../../src/util/syntax-helpers'; +import { JSAst } from '../../../src/syntax'; +import { NamedNodeMutation } from '../../../src/mutant'; describe('babel-transformer', () => { let context: sinon.SinonStubbedInstance; @@ -169,6 +173,82 @@ describe('babel-transformer', () => { expectMutateNotCalledWith((t) => t.isDecorator()); }); + describe('with mutationRanges', () => { + let ast: JSAst; + let mutant: NamedNodeMutation; + + beforeEach(() => { + ast = createJSAst({ + originFileName: 'foo.js', + rawContent: + 'console.log("line 1");\n' + + 'console.log("line 2");\n' + + '{\n' + + 'console.log("line 4");\n' + + 'console.log("line 5");\n' + + '}\n' + + 'console.log("line 6");\n', + }); + mutant = createNamedNodeMutation({ original: types.identifier('first') }); + mutateStub.onFirstCall().returns([mutant]); + }); + + function range(startLine: number, startColumn: number, endLine: number, endColumn: number, fileName = 'foo.js') { + return { + fileName, + start: { line: startLine, column: startColumn }, + end: { line: endLine, column: endColumn }, + }; + } + + it('should mutate a node that matches the a single line range', () => { + context.options.mutationRanges = [range(4, 12, 4, 20)]; + transformBabel(ast, mutantCollectorMock, context); + expect(mutateStub.firstCall.args[0].toString()).eq('"line 4"'); + }); + + it('should not mutate a node that does not match a single line start range', () => { + context.options.mutationRanges = [range(4, 13, 4, 20)]; + transformBabel(ast, mutantCollectorMock, context); + expect(mutateStub).not.called; + }); + + it('should not mutate a node that does not match a single line end range', () => { + context.options.mutationRanges = [range(4, 12, 4, 19)]; + transformBabel(ast, mutantCollectorMock, context); + expect(mutateStub).not.called; + }); + + it('should mutate a node that matches a multi line range', () => { + context.options.mutationRanges = [range(3, 0, 7, 0)]; + transformBabel(ast, mutantCollectorMock, context); + expect(normalizeWhitespaces(mutateStub.firstCall.args[0].toString())).eq('{ console.log("line 4"); console.log("line 5"); }'); + expect(normalizeWhitespaces(mutateStub.secondCall.args[0].toString())).eq('console.log("line 4");'); + }); + + it('should not mutate a node is not in the start line range', () => { + context.options.mutationRanges = [range(4, 0, 7, 0)]; + transformBabel(ast, mutantCollectorMock, context); + mutateStub.getCalls().forEach((call) => { + expect(normalizeWhitespaces(call.args[0].toString())).not.eq('{ console.log("line 4"); console.log("line 5"); }'); + }); + }); + + it('should not mutate a node is not in the end line range', () => { + context.options.mutationRanges = [range(2, 0, 6, 0)]; + transformBabel(ast, mutantCollectorMock, context); + mutateStub.getCalls().forEach((call) => { + expect(normalizeWhitespaces(call.args[0].toString())).not.eq('{ console.log("line 4"); console.log("line 5"); }'); + }); + }); + + it('should still mutate other files', () => { + context.options.mutationRanges = [range(100, 0, 101, 0, 'bar.js')]; + transformBabel(ast, mutantCollectorMock, context); + expect(mutantCollectorMock.add).calledWith('foo.js', mutant); + }); + }); + function expectMutateNotCalledWith(predicate: (nodePath: NodePath) => boolean) { mutateStub.getCalls().forEach((call) => { const nodePath: NodePath = call.args[0]; diff --git a/packages/instrumenter/testResources/instrumenter/specific-mutants.ts b/packages/instrumenter/testResources/instrumenter/specific-mutants.ts new file mode 100644 index 0000000000..5f4b40bc13 --- /dev/null +++ b/packages/instrumenter/testResources/instrumenter/specific-mutants.ts @@ -0,0 +1,20 @@ +const a = 1 + 1; +const b = 1 - 1; + +if (a === 2 && b === 0) { + console.log('a'); +} + +if (a === 2 && b === 0) { + console.log('b'); +} + +const itemWithLongName = { + longPropertyName1: 1, + longPropertyName2: 2, + longPropertyName3: 3, +}; + +const item = () => + itemWithLongName.longPropertyName1 === itemWithLongName.longPropertyName2 && + itemWithLongName.longPropertyName1 === itemWithLongName.longPropertyName3; diff --git a/packages/instrumenter/testResources/instrumenter/specific-mutants.ts.out.snap b/packages/instrumenter/testResources/instrumenter/specific-mutants.ts.out.snap new file mode 100644 index 0000000000..5d4254d371 --- /dev/null +++ b/packages/instrumenter/testResources/instrumenter/specific-mutants.ts.out.snap @@ -0,0 +1,76 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`instrumenter integration with mutation ranges should only mutate specific mutants for the given file 1`] = ` +"function stryNS_9fa48() { + var g = new Function(\\"return this\\")(); + var ns = g.__stryker__ || (g.__stryker__ = {}); + + if (ns.activeMutant === undefined && g.process && g.process.env && g.process.env.__STRYKER_ACTIVE_MUTANT__) { + ns.activeMutant = Number(g.process.env.__STRYKER_ACTIVE_MUTANT__); + } + + function retrieveNS() { + return ns; + } + + stryNS_9fa48 = retrieveNS; + return retrieveNS(); +} + +stryNS_9fa48(); + +function stryCov_9fa48() { + var ns = stryNS_9fa48(); + var cov = ns.mutantCoverage || (ns.mutantCoverage = { + static: {}, + perTest: {} + }); + + function cover() { + var c = cov.static; + + if (ns.currentTestId) { + c = cov.perTest[ns.currentTestId] = cov.perTest[ns.currentTestId] || {}; + } + + var a = arguments; + + for (var i = 0; i < a.length; i++) { + c[a[i]] = (c[a[i]] || 0) + 1; + } + } + + stryCov_9fa48 = cover; + cover.apply(null, arguments); +} + +function stryMutAct_9fa48(id) { + var ns = stryNS_9fa48(); + + function isActive(id) { + return ns.activeMutant === id; + } + + stryMutAct_9fa48 = isActive; + return isActive(id); +} + +const a = stryMutAct_9fa48(0) ? 1 - 1 : (stryCov_9fa48(0), 1 + 1); +const b = 1 - 1; + +if ((stryMutAct_9fa48(3) ? a !== 2 : stryMutAct_9fa48(2) ? false : stryMutAct_9fa48(1) ? true : (stryCov_9fa48(1, 2, 3), a === 2)) && b === 0) { + console.log('a'); +} + +if (a === 2 && (stryMutAct_9fa48(6) ? b !== 0 : stryMutAct_9fa48(5) ? false : stryMutAct_9fa48(4) ? true : (stryCov_9fa48(4, 5, 6), b === 0))) { + console.log('b'); +} + +const itemWithLongName = { + longPropertyName1: 1, + longPropertyName2: 2, + longPropertyName3: 3 +}; + +const item = () => stryMutAct_9fa48(9) ? itemWithLongName.longPropertyName1 === itemWithLongName.longPropertyName2 || itemWithLongName.longPropertyName1 === itemWithLongName.longPropertyName3 : stryMutAct_9fa48(8) ? false : stryMutAct_9fa48(7) ? true : (stryCov_9fa48(7, 8, 9), (stryMutAct_9fa48(12) ? itemWithLongName.longPropertyName1 !== itemWithLongName.longPropertyName2 : stryMutAct_9fa48(11) ? false : stryMutAct_9fa48(10) ? true : (stryCov_9fa48(10, 11, 12), itemWithLongName.longPropertyName1 === itemWithLongName.longPropertyName2)) && (stryMutAct_9fa48(15) ? itemWithLongName.longPropertyName1 !== itemWithLongName.longPropertyName3 : stryMutAct_9fa48(14) ? false : stryMutAct_9fa48(13) ? true : (stryCov_9fa48(13, 14, 15), itemWithLongName.longPropertyName1 === itemWithLongName.longPropertyName3)));" +`; diff --git a/packages/instrumenter/testResources/instrumenter/specific-no-mutants.ts b/packages/instrumenter/testResources/instrumenter/specific-no-mutants.ts new file mode 100644 index 0000000000..f381a761d4 --- /dev/null +++ b/packages/instrumenter/testResources/instrumenter/specific-no-mutants.ts @@ -0,0 +1,2 @@ +const a = 1 + 1; +const b = 1 - 1; diff --git a/packages/instrumenter/testResources/instrumenter/specific-no-mutants.ts.out.snap b/packages/instrumenter/testResources/instrumenter/specific-no-mutants.ts.out.snap new file mode 100644 index 0000000000..e978c86d60 --- /dev/null +++ b/packages/instrumenter/testResources/instrumenter/specific-no-mutants.ts.out.snap @@ -0,0 +1,60 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`instrumenter integration with mutation ranges should not make any mutations in a file not found in the specific mutants 1`] = ` +"function stryNS_9fa48() { + var g = new Function(\\"return this\\")(); + var ns = g.__stryker__ || (g.__stryker__ = {}); + + if (ns.activeMutant === undefined && g.process && g.process.env && g.process.env.__STRYKER_ACTIVE_MUTANT__) { + ns.activeMutant = Number(g.process.env.__STRYKER_ACTIVE_MUTANT__); + } + + function retrieveNS() { + return ns; + } + + stryNS_9fa48 = retrieveNS; + return retrieveNS(); +} + +stryNS_9fa48(); + +function stryCov_9fa48() { + var ns = stryNS_9fa48(); + var cov = ns.mutantCoverage || (ns.mutantCoverage = { + static: {}, + perTest: {} + }); + + function cover() { + var c = cov.static; + + if (ns.currentTestId) { + c = cov.perTest[ns.currentTestId] = cov.perTest[ns.currentTestId] || {}; + } + + var a = arguments; + + for (var i = 0; i < a.length; i++) { + c[a[i]] = (c[a[i]] || 0) + 1; + } + } + + stryCov_9fa48 = cover; + cover.apply(null, arguments); +} + +function stryMutAct_9fa48(id) { + var ns = stryNS_9fa48(); + + function isActive(id) { + return ns.activeMutant === id; + } + + stryMutAct_9fa48 = isActive; + return isActive(id); +} + +const a = stryMutAct_9fa48(0) ? 1 - 1 : (stryCov_9fa48(0), 1 + 1); +const b = stryMutAct_9fa48(1) ? 1 + 1 : (stryCov_9fa48(1), 1 - 1);" +`;