@systemfsoftware/stryker-js-typescript-checker 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +15 -0
- package/.turbo/turbo-lint.log +3 -0
- package/LICENSE +21 -0
- package/dist/index.d.mts +122 -0
- package/dist/index.mjs +602 -0
- package/oxlint.config.ts +9 -0
- package/package.json +37 -0
- package/schema/typescript-checker-options.json +22 -0
- package/src/fs/hybrid-file-system.ts +118 -0
- package/src/fs/index.ts +2 -0
- package/src/fs/script-file.ts +44 -0
- package/src/grouping/create-groups.ts +77 -0
- package/src/grouping/ts-file-node.ts +54 -0
- package/src/index.ts +18 -0
- package/src/plugin-tokens.ts +2 -0
- package/src/tsconfig-helpers.ts +168 -0
- package/src/typescript-checker-options-with-stryker-options.ts +9 -0
- package/src/typescript-checker.ts +223 -0
- package/src/typescript-compiler.ts +406 -0
- package/test/integration/e2e-plugin-entry.it.spec.ts +153 -0
- package/test/integration/project-references.it.spec.ts +146 -0
- package/test/integration/project-with-ts-buildinfo.it.spec.ts +92 -0
- package/test/integration/single-project.it.spec.ts +263 -0
- package/test/integration/typescript-checkers-errors.it.spec.ts +87 -0
- package/test/unit/fs/hybrid-file-system.spec.ts +109 -0
- package/test/unit/grouping/create-groups.spec.ts +122 -0
- package/test/unit/grouping/ts-file-node.spec.ts +132 -0
- package/testResources/errors/compile-error/add.ts +3 -0
- package/testResources/errors/compile-error/tsconfig.json +6 -0
- package/testResources/errors/empty-dir/.gitkeep +0 -0
- package/testResources/errors/invalid-tsconfig/tsconfig.json +1 -0
- package/testResources/project-references/src/index.ts +5 -0
- package/testResources/project-references/src/job.ts +6 -0
- package/testResources/project-references/src/src.tsbuildinfo +1 -0
- package/testResources/project-references/src/tsconfig.json +10 -0
- package/testResources/project-references/tsconfig.root.json +7 -0
- package/testResources/project-references/tsconfig.settings.json +17 -0
- package/testResources/project-references/utils/math.ts +3 -0
- package/testResources/project-references/utils/text.ts +3 -0
- package/testResources/project-references/utils/tsconfig.json +7 -0
- package/testResources/project-references/utils/utils.tsbuildinfo +1 -0
- package/testResources/project-with-ts-buildinfo/do-not-delete.tsbuildinfo +1 -0
- package/testResources/project-with-ts-buildinfo/src/index.ts +3 -0
- package/testResources/project-with-ts-buildinfo/tsconfig.json +17 -0
- package/testResources/single-project/src/counter.ts +9 -0
- package/testResources/single-project/src/errorInFileAbove2Mutants/counter.ts +9 -0
- package/testResources/single-project/src/errorInFileAbove2Mutants/todo-counter.ts +7 -0
- package/testResources/single-project/src/errorInFileAbove2Mutants/todo.spec.ts +11 -0
- package/testResources/single-project/src/errorInFileAbove2Mutants/todo.ts +22 -0
- package/testResources/single-project/src/not-type-checked.js +1 -0
- package/testResources/single-project/src/todo.spec.ts +11 -0
- package/testResources/single-project/src/todo.ts +22 -0
- package/testResources/single-project/tsconfig.json +15 -0
- package/tsconfig.json +10 -0
- package/vitest.config.ts +11 -0
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import path from 'path'
|
|
2
|
+
import { fileURLToPath } from 'url'
|
|
3
|
+
|
|
4
|
+
import { CheckStatus } from '@stryker-mutator/api/check'
|
|
5
|
+
import type { Mutant, StrykerOptions } from '@stryker-mutator/api/core'
|
|
6
|
+
import type { Logger, LoggerFactoryMethod } from '@stryker-mutator/api/logging'
|
|
7
|
+
import { commonTokens } from '@stryker-mutator/api/plugin'
|
|
8
|
+
import type { Injector, PluginContext } from '@stryker-mutator/api/plugin'
|
|
9
|
+
import { afterEach, beforeEach, describe, expect, it } from 'vitest'
|
|
10
|
+
|
|
11
|
+
import { HybridFileSystem } from '../../src/fs/hybrid-file-system.js'
|
|
12
|
+
import { createTypescriptChecker } from '../../src/index.js'
|
|
13
|
+
import * as pluginTokens from '../../src/plugin-tokens.js'
|
|
14
|
+
import { TypescriptChecker } from '../../src/typescript-checker.js'
|
|
15
|
+
import { TypescriptCompiler } from '../../src/typescript-compiler.js'
|
|
16
|
+
|
|
17
|
+
const resolveTestResource = path.resolve.bind(
|
|
18
|
+
path,
|
|
19
|
+
path.dirname(fileURLToPath(import.meta.url)),
|
|
20
|
+
'..',
|
|
21
|
+
'..',
|
|
22
|
+
'testResources',
|
|
23
|
+
'single-project',
|
|
24
|
+
) as unknown as typeof path.resolve
|
|
25
|
+
|
|
26
|
+
function createLogger(): Logger {
|
|
27
|
+
return {
|
|
28
|
+
isTraceEnabled: () => false,
|
|
29
|
+
isDebugEnabled: () => false,
|
|
30
|
+
isInfoEnabled: () => false,
|
|
31
|
+
isWarnEnabled: () => false,
|
|
32
|
+
isErrorEnabled: () => false,
|
|
33
|
+
isFatalEnabled: () => false,
|
|
34
|
+
trace: () => {},
|
|
35
|
+
debug: () => {},
|
|
36
|
+
info: () => {},
|
|
37
|
+
warn: () => {},
|
|
38
|
+
error: () => {},
|
|
39
|
+
fatal: () => {},
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
interface InjectableFunctionWithTokens {
|
|
44
|
+
inject?: readonly string[]
|
|
45
|
+
(...args: unknown[]): unknown
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
interface InjectableClassWithTokens {
|
|
49
|
+
inject?: readonly string[]
|
|
50
|
+
new(...args: unknown[]): unknown
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function createInjector(tsconfigFile: string): Injector<PluginContext> {
|
|
54
|
+
const context: Record<string, unknown> = {
|
|
55
|
+
[commonTokens.getLogger]: (): Logger => createLogger(),
|
|
56
|
+
[commonTokens.logger]: createLogger(),
|
|
57
|
+
[commonTokens.options]: {
|
|
58
|
+
tsconfigFile,
|
|
59
|
+
typescriptChecker: { prioritizePerformanceOverAccuracy: true },
|
|
60
|
+
} as unknown as StrykerOptions,
|
|
61
|
+
[commonTokens.target]: undefined,
|
|
62
|
+
[commonTokens.injector]: undefined,
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const resolveToken = (token: string): unknown => {
|
|
66
|
+
if (!(token in context)) {
|
|
67
|
+
throw new Error(`Missing injection token: ${token}`)
|
|
68
|
+
}
|
|
69
|
+
return context[token]
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const injectArgs = (inject: readonly string[]): unknown[] => inject.map(resolveToken)
|
|
73
|
+
|
|
74
|
+
const self: Injector<PluginContext> = {
|
|
75
|
+
provideFactory: (token, factory, _scope) => {
|
|
76
|
+
const f = factory as InjectableFunctionWithTokens
|
|
77
|
+
context[token] = f(...injectArgs(f.inject ?? []))
|
|
78
|
+
return self
|
|
79
|
+
},
|
|
80
|
+
provideClass: (token, Class, _scope) => {
|
|
81
|
+
const C = Class as InjectableClassWithTokens
|
|
82
|
+
context[token] = new C(...injectArgs(C.inject ?? []))
|
|
83
|
+
return self
|
|
84
|
+
},
|
|
85
|
+
injectClass: (Class) => {
|
|
86
|
+
const C = Class as InjectableClassWithTokens
|
|
87
|
+
return new C(...injectArgs(C.inject ?? [])) as never
|
|
88
|
+
},
|
|
89
|
+
injectFunction: (fn) => {
|
|
90
|
+
const f = fn as InjectableFunctionWithTokens
|
|
91
|
+
return f(...injectArgs(f.inject ?? [])) as never
|
|
92
|
+
},
|
|
93
|
+
resolve: (token) => resolveToken(token as string) as never,
|
|
94
|
+
provideValue: (token, value) => {
|
|
95
|
+
context[token] = value
|
|
96
|
+
return self
|
|
97
|
+
},
|
|
98
|
+
createChildInjector: () => self,
|
|
99
|
+
dispose: async () => {},
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return self
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
interface ClosableChecker {
|
|
106
|
+
tsCompiler: { close(): void }
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const todoSource =
|
|
110
|
+
'export interface ITodo {\n name: string;\n description: string;\n completed: boolean;\n}\n\nclass Todo implements ITodo {\n constructor(public name: string, public description: string, public completed: boolean) { }\n}\n\nexport class TodoList {\n public static allTodos: Todo[] = [];\n createTodoItem(name: string, description: string) {\n let newItem = new Todo(name, description, false);\n let totalCount: number = TodoList.allTodos.push(newItem);\n return totalCount;\n }\n\n allTodoItems(): ITodo[] {\n return TodoList.allTodos;\n }\n}\n\n'
|
|
111
|
+
|
|
112
|
+
function createMutant(replacement: string, id: string): Mutant {
|
|
113
|
+
const findText = 'TodoList.allTodos.push(newItem)'
|
|
114
|
+
const lines = todoSource.split('\n')
|
|
115
|
+
const lineNumber = lines.findIndex((line) => line.includes(findText))
|
|
116
|
+
const textColumn = lines[lineNumber]!.indexOf(findText)
|
|
117
|
+
return {
|
|
118
|
+
id,
|
|
119
|
+
fileName: resolveTestResource('src', 'todo.ts'),
|
|
120
|
+
mutatorName: 'foo-mutator',
|
|
121
|
+
location: {
|
|
122
|
+
start: { line: lineNumber, column: textColumn },
|
|
123
|
+
end: { line: lineNumber, column: textColumn + findText.length },
|
|
124
|
+
},
|
|
125
|
+
replacement,
|
|
126
|
+
} as unknown as Mutant
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
describe('Typescript checker plugin entry', () => {
|
|
130
|
+
let sut: TypescriptChecker
|
|
131
|
+
|
|
132
|
+
beforeEach(() => {
|
|
133
|
+
const injector = createInjector(resolveTestResource('tsconfig.json'))
|
|
134
|
+
sut = createTypescriptChecker(injector)
|
|
135
|
+
return sut.init()
|
|
136
|
+
})
|
|
137
|
+
|
|
138
|
+
afterEach(() => {
|
|
139
|
+
;(sut as unknown as ClosableChecker).tsCompiler.close()
|
|
140
|
+
})
|
|
141
|
+
|
|
142
|
+
it('should report Passed for a mutant without compile errors', async () => {
|
|
143
|
+
const mutant = createMutant('newItem ? 42 : 43', 'e2e-pass')
|
|
144
|
+
const actual = await sut.check([mutant])
|
|
145
|
+
expect(actual).toEqual({ 'e2e-pass': { status: CheckStatus.Passed } })
|
|
146
|
+
})
|
|
147
|
+
|
|
148
|
+
it('should report CompileError for a mutant that introduces a type error', async () => {
|
|
149
|
+
const mutant = createMutant('TodoList.allTodos.push("invalid")', 'e2e-fail')
|
|
150
|
+
const actual = await sut.check([mutant])
|
|
151
|
+
expect(actual['e2e-fail']?.status).toBe(CheckStatus.CompileError)
|
|
152
|
+
})
|
|
153
|
+
})
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import fs from 'fs'
|
|
2
|
+
import path from 'path'
|
|
3
|
+
import { fileURLToPath } from 'url'
|
|
4
|
+
|
|
5
|
+
import { CheckStatus } from '@stryker-mutator/api/check'
|
|
6
|
+
import type { Location, Mutant, StrykerOptions } from '@stryker-mutator/api/core'
|
|
7
|
+
import type { Logger } from '@stryker-mutator/api/logging'
|
|
8
|
+
import { afterEach, beforeEach, describe, expect, it } from 'vitest'
|
|
9
|
+
|
|
10
|
+
import { HybridFileSystem } from '../../src/fs/hybrid-file-system.js'
|
|
11
|
+
import { TypescriptChecker } from '../../src/typescript-checker.js'
|
|
12
|
+
import { TypescriptCompiler } from '../../src/typescript-compiler.js'
|
|
13
|
+
|
|
14
|
+
const resolveTestResource = path.resolve.bind(
|
|
15
|
+
path,
|
|
16
|
+
path.dirname(fileURLToPath(import.meta.url)),
|
|
17
|
+
'..',
|
|
18
|
+
'..',
|
|
19
|
+
'testResources',
|
|
20
|
+
'project-references',
|
|
21
|
+
) as unknown as typeof path.resolve
|
|
22
|
+
|
|
23
|
+
function createLogger(): Logger {
|
|
24
|
+
return {
|
|
25
|
+
isTraceEnabled: () => false,
|
|
26
|
+
isDebugEnabled: () => false,
|
|
27
|
+
isInfoEnabled: () => false,
|
|
28
|
+
isWarnEnabled: () => false,
|
|
29
|
+
isErrorEnabled: () => false,
|
|
30
|
+
isFatalEnabled: () => false,
|
|
31
|
+
trace: () => {},
|
|
32
|
+
debug: () => {},
|
|
33
|
+
info: () => {},
|
|
34
|
+
warn: () => {},
|
|
35
|
+
error: () => {},
|
|
36
|
+
fatal: () => {},
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function createChecker(tsconfigFile: string): TypescriptChecker {
|
|
41
|
+
const options = {
|
|
42
|
+
tsconfigFile,
|
|
43
|
+
typescriptChecker: { prioritizePerformanceOverAccuracy: true },
|
|
44
|
+
} as unknown as StrykerOptions
|
|
45
|
+
const logger = createLogger()
|
|
46
|
+
const fileSystem = new HybridFileSystem()
|
|
47
|
+
const compiler = new TypescriptCompiler(logger, options, fileSystem)
|
|
48
|
+
return new TypescriptChecker(logger, options, compiler)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
describe('Typescript checker on a project with project references', () => {
|
|
52
|
+
let sut: TypescriptChecker
|
|
53
|
+
|
|
54
|
+
beforeEach(() => {
|
|
55
|
+
sut = createChecker(resolveTestResource('tsconfig.root.json'))
|
|
56
|
+
return sut.init()
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
afterEach(() => {
|
|
60
|
+
// @ts-expect-error private close method
|
|
61
|
+
sut.tsCompiler.close()
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
it('should not write output to disk', () => {
|
|
65
|
+
expect(
|
|
66
|
+
fs.existsSync(resolveTestResource('dist')),
|
|
67
|
+
'Output was written to disk!',
|
|
68
|
+
).toBe(false)
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
it('should be able to validate a mutant', async () => {
|
|
72
|
+
const mutant = createMutant('job.ts', 'Starting job', 'stryker was here')
|
|
73
|
+
const actualResult = await sut.check([mutant])
|
|
74
|
+
expect(actualResult[mutant.id]!).toEqual({ status: CheckStatus.Passed })
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
it('should allow unused local variables (override options)', async () => {
|
|
78
|
+
const mutant = createMutant(
|
|
79
|
+
'job.ts',
|
|
80
|
+
'toUpperCase(logText)',
|
|
81
|
+
'toUpperCase("")',
|
|
82
|
+
)
|
|
83
|
+
const actual = await sut.check([mutant])
|
|
84
|
+
expect(actual[mutant.id]!).toEqual({ status: CheckStatus.Passed })
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
it('should create multiple groups if reference between project', async () => {
|
|
88
|
+
const mutantInSourceProject = createMutant(
|
|
89
|
+
'job.ts',
|
|
90
|
+
'Starting job',
|
|
91
|
+
'',
|
|
92
|
+
'42',
|
|
93
|
+
)
|
|
94
|
+
const mutantInProjectWithReference = createMutant(
|
|
95
|
+
'text.ts',
|
|
96
|
+
'toUpperCase()',
|
|
97
|
+
'toLowerCase()',
|
|
98
|
+
'43',
|
|
99
|
+
)
|
|
100
|
+
const mutantOutsideOfReference = createMutant(
|
|
101
|
+
'math.ts',
|
|
102
|
+
'array.length',
|
|
103
|
+
'1',
|
|
104
|
+
'44',
|
|
105
|
+
)
|
|
106
|
+
const result = await sut.group([
|
|
107
|
+
mutantInSourceProject,
|
|
108
|
+
mutantInProjectWithReference,
|
|
109
|
+
mutantOutsideOfReference,
|
|
110
|
+
])
|
|
111
|
+
expect(result).toHaveLength(2)
|
|
112
|
+
})
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
const fileContents: Record<string, string> = Object.freeze({
|
|
116
|
+
['index.ts']: fs.readFileSync(resolveTestResource('src', 'index.ts'), 'utf8'),
|
|
117
|
+
['job.ts']: fs.readFileSync(resolveTestResource('src', 'job.ts'), 'utf8'),
|
|
118
|
+
['math.ts']: fs.readFileSync(resolveTestResource('utils', 'math.ts'), 'utf8'),
|
|
119
|
+
['text.ts']: fs.readFileSync(resolveTestResource('utils', 'text.ts'), 'utf8'),
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
function createMutant(
|
|
123
|
+
fileName: 'index.ts' | 'job.ts' | 'math.ts' | 'text.ts',
|
|
124
|
+
findText: string,
|
|
125
|
+
replacement: string,
|
|
126
|
+
id = '42',
|
|
127
|
+
offset = 0,
|
|
128
|
+
): Mutant {
|
|
129
|
+
const lines = fileContents[fileName]!.split('\n')
|
|
130
|
+
const lineNumber = lines.findIndex((l) => l.includes(findText))
|
|
131
|
+
if (lineNumber === -1) {
|
|
132
|
+
throw new Error(`Cannot find ${findText} in ${fileName}`)
|
|
133
|
+
}
|
|
134
|
+
const textColumn = lines[lineNumber]!.indexOf(findText)
|
|
135
|
+
const location: Location = {
|
|
136
|
+
start: { line: lineNumber, column: textColumn + offset },
|
|
137
|
+
end: { line: lineNumber, column: textColumn + findText.length },
|
|
138
|
+
}
|
|
139
|
+
return {
|
|
140
|
+
id,
|
|
141
|
+
fileName: resolveTestResource('src', fileName),
|
|
142
|
+
mutatorName: 'foo-mutator',
|
|
143
|
+
location,
|
|
144
|
+
replacement,
|
|
145
|
+
}
|
|
146
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import fs from 'fs'
|
|
2
|
+
import path from 'path'
|
|
3
|
+
import { fileURLToPath } from 'url'
|
|
4
|
+
|
|
5
|
+
import type { Location, Mutant, StrykerOptions } from '@stryker-mutator/api/core'
|
|
6
|
+
import type { Logger } from '@stryker-mutator/api/logging'
|
|
7
|
+
import { describe, expect, it } from 'vitest'
|
|
8
|
+
|
|
9
|
+
import { HybridFileSystem } from '../../src/fs/hybrid-file-system.js'
|
|
10
|
+
import { TypescriptChecker } from '../../src/typescript-checker.js'
|
|
11
|
+
import { TypescriptCompiler } from '../../src/typescript-compiler.js'
|
|
12
|
+
|
|
13
|
+
const resolveTestResource = path.resolve.bind(
|
|
14
|
+
path,
|
|
15
|
+
path.dirname(fileURLToPath(import.meta.url)),
|
|
16
|
+
'..',
|
|
17
|
+
'..',
|
|
18
|
+
'testResources',
|
|
19
|
+
'project-with-ts-buildinfo',
|
|
20
|
+
) as unknown as typeof path.resolve
|
|
21
|
+
|
|
22
|
+
function createLogger(): Logger {
|
|
23
|
+
return {
|
|
24
|
+
isTraceEnabled: () => false,
|
|
25
|
+
isDebugEnabled: () => false,
|
|
26
|
+
isInfoEnabled: () => false,
|
|
27
|
+
isWarnEnabled: () => false,
|
|
28
|
+
isErrorEnabled: () => false,
|
|
29
|
+
isFatalEnabled: () => false,
|
|
30
|
+
trace: () => {},
|
|
31
|
+
debug: () => {},
|
|
32
|
+
info: () => {},
|
|
33
|
+
warn: () => {},
|
|
34
|
+
error: () => {},
|
|
35
|
+
fatal: () => {},
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function createChecker(tsconfigFile: string): TypescriptChecker {
|
|
40
|
+
const options = {
|
|
41
|
+
tsconfigFile,
|
|
42
|
+
typescriptChecker: { prioritizePerformanceOverAccuracy: true },
|
|
43
|
+
} as unknown as StrykerOptions
|
|
44
|
+
const logger = createLogger()
|
|
45
|
+
const fileSystem = new HybridFileSystem()
|
|
46
|
+
const compiler = new TypescriptCompiler(logger, options, fileSystem)
|
|
47
|
+
return new TypescriptChecker(logger, options, compiler)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
describe('project-with-ts-buildinfo', () => {
|
|
51
|
+
it('should load project on init', async () => {
|
|
52
|
+
const sut = createChecker(resolveTestResource('tsconfig.json'))
|
|
53
|
+
await sut.init()
|
|
54
|
+
const group = await sut.group([createMutant('src/index.ts', '', '')])
|
|
55
|
+
expect(group).toHaveLength(1)
|
|
56
|
+
// @ts-expect-error private close method
|
|
57
|
+
sut.tsCompiler.close()
|
|
58
|
+
})
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
const fileContents: Record<string, string> = Object.freeze({
|
|
62
|
+
['src/index.ts']: fs.readFileSync(
|
|
63
|
+
resolveTestResource('src', 'index.ts'),
|
|
64
|
+
'utf8',
|
|
65
|
+
),
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
function createMutant(
|
|
69
|
+
fileName: 'src/index.ts',
|
|
70
|
+
findText: string,
|
|
71
|
+
replacement: string,
|
|
72
|
+
id = '42',
|
|
73
|
+
offset = 0,
|
|
74
|
+
): Mutant {
|
|
75
|
+
const lines = fileContents[fileName]!.split('\n')
|
|
76
|
+
const lineNumber = lines.findIndex((l) => l.includes(findText))
|
|
77
|
+
if (lineNumber === -1) {
|
|
78
|
+
throw new Error(`Cannot find ${findText} in ${fileName}`)
|
|
79
|
+
}
|
|
80
|
+
const textColumn = lines[lineNumber]!.indexOf(findText)
|
|
81
|
+
const location: Location = {
|
|
82
|
+
start: { line: lineNumber, column: textColumn + offset },
|
|
83
|
+
end: { line: lineNumber, column: textColumn + findText.length },
|
|
84
|
+
}
|
|
85
|
+
return {
|
|
86
|
+
id,
|
|
87
|
+
fileName: resolveTestResource('src', fileName),
|
|
88
|
+
mutatorName: 'foo-mutator',
|
|
89
|
+
location,
|
|
90
|
+
replacement,
|
|
91
|
+
}
|
|
92
|
+
}
|
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
import fs from 'fs'
|
|
2
|
+
import path from 'path'
|
|
3
|
+
import { fileURLToPath } from 'url'
|
|
4
|
+
|
|
5
|
+
import { CheckStatus } from '@stryker-mutator/api/check'
|
|
6
|
+
import type { FailedCheckResult } from '@stryker-mutator/api/check'
|
|
7
|
+
import type { Location, Mutant, StrykerOptions } from '@stryker-mutator/api/core'
|
|
8
|
+
import type { Logger } from '@stryker-mutator/api/logging'
|
|
9
|
+
import { afterEach, beforeEach, describe, expect, it } from 'vitest'
|
|
10
|
+
|
|
11
|
+
import { HybridFileSystem } from '../../src/fs/hybrid-file-system.js'
|
|
12
|
+
import { TypescriptChecker } from '../../src/typescript-checker.js'
|
|
13
|
+
import { TypescriptCompiler } from '../../src/typescript-compiler.js'
|
|
14
|
+
|
|
15
|
+
const resolveTestResource = path.resolve.bind(
|
|
16
|
+
path,
|
|
17
|
+
path.dirname(fileURLToPath(import.meta.url)),
|
|
18
|
+
'..',
|
|
19
|
+
'..',
|
|
20
|
+
'testResources',
|
|
21
|
+
'single-project',
|
|
22
|
+
) as unknown as typeof path.resolve
|
|
23
|
+
|
|
24
|
+
function createLogger(): Logger {
|
|
25
|
+
return {
|
|
26
|
+
isTraceEnabled: () => false,
|
|
27
|
+
isDebugEnabled: () => false,
|
|
28
|
+
isInfoEnabled: () => false,
|
|
29
|
+
isWarnEnabled: () => false,
|
|
30
|
+
isErrorEnabled: () => false,
|
|
31
|
+
isFatalEnabled: () => false,
|
|
32
|
+
trace: () => {},
|
|
33
|
+
debug: () => {},
|
|
34
|
+
info: () => {},
|
|
35
|
+
warn: () => {},
|
|
36
|
+
error: () => {},
|
|
37
|
+
fatal: () => {},
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function createChecker(tsconfigFile: string): TypescriptChecker {
|
|
42
|
+
const options = {
|
|
43
|
+
tsconfigFile,
|
|
44
|
+
typescriptChecker: { prioritizePerformanceOverAccuracy: true },
|
|
45
|
+
} as unknown as StrykerOptions
|
|
46
|
+
const logger = createLogger()
|
|
47
|
+
const fileSystem = new HybridFileSystem()
|
|
48
|
+
const compiler = new TypescriptCompiler(logger, options, fileSystem)
|
|
49
|
+
return new TypescriptChecker(logger, options, compiler)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
describe('Typescript checker on a single project', () => {
|
|
53
|
+
let sut: TypescriptChecker
|
|
54
|
+
|
|
55
|
+
beforeEach(() => {
|
|
56
|
+
sut = createChecker(resolveTestResource('tsconfig.json'))
|
|
57
|
+
return sut.init()
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
afterEach(() => {
|
|
61
|
+
// @ts-expect-error private close method
|
|
62
|
+
sut.tsCompiler.close()
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
it('should not write output to disk', () => {
|
|
66
|
+
expect(
|
|
67
|
+
fs.existsSync(resolveTestResource('dist')),
|
|
68
|
+
'Output was written to disk!',
|
|
69
|
+
).toBe(false)
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
it('should be able to validate a mutant that does not result in an error', async () => {
|
|
73
|
+
const mutant = createMutant(
|
|
74
|
+
'todo.ts',
|
|
75
|
+
'TodoList.allTodos.push(newItem)',
|
|
76
|
+
'newItem? 42: 43',
|
|
77
|
+
'42',
|
|
78
|
+
)
|
|
79
|
+
const actual = await sut.check([mutant])
|
|
80
|
+
expect(actual).toEqual({ '42': { status: CheckStatus.Passed } })
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
it('should be able invalidate a mutant that does result in a compile error', async () => {
|
|
84
|
+
const mutant = createMutant(
|
|
85
|
+
'todo.ts',
|
|
86
|
+
'TodoList.allTodos.push(newItem)',
|
|
87
|
+
'"This should not be a string 🙄"',
|
|
88
|
+
'mutId',
|
|
89
|
+
)
|
|
90
|
+
const actual = await sut.check([mutant])
|
|
91
|
+
expect(actual['mutId']!.status).toBe(CheckStatus.CompileError)
|
|
92
|
+
expect((actual['mutId'] as FailedCheckResult).reason).toContain(
|
|
93
|
+
'todo.ts(15,9): error TS2322',
|
|
94
|
+
)
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
it('should be able validate a mutant that does not result in a compile error after a compile error', async () => {
|
|
98
|
+
const mutantCompileError = createMutant(
|
|
99
|
+
'todo.ts',
|
|
100
|
+
'TodoList.allTodos.push(newItem)',
|
|
101
|
+
'"This should not be a string 🙄"',
|
|
102
|
+
)
|
|
103
|
+
const mutantWithoutError = createMutant(
|
|
104
|
+
'todo.ts',
|
|
105
|
+
'return TodoList.allTodos',
|
|
106
|
+
'[]',
|
|
107
|
+
'mut42',
|
|
108
|
+
7,
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
await sut.check([mutantCompileError])
|
|
112
|
+
const actual = await sut.check([mutantWithoutError])
|
|
113
|
+
|
|
114
|
+
expect(actual).toEqual({ mut42: { status: CheckStatus.Passed } })
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
it('should be able to invalidate a mutant that results in an error in a different file', async () => {
|
|
118
|
+
const actual = await sut.check([
|
|
119
|
+
createMutant('todo.ts', 'return totalCount', '', '42'),
|
|
120
|
+
])
|
|
121
|
+
expect(actual['42']!.status).toBe(CheckStatus.CompileError)
|
|
122
|
+
expect((actual['42'] as FailedCheckResult).reason).toContain(
|
|
123
|
+
'todo.spec.ts(4,7): error TS2322',
|
|
124
|
+
)
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
it('should be able to validate a mutant after a mutant in a different file resulted in a transpile error', async () => {
|
|
128
|
+
await sut.check([createMutant('todo.ts', 'return totalCount', '')])
|
|
129
|
+
const result = await sut.check([
|
|
130
|
+
createMutant(
|
|
131
|
+
'todo.spec.ts',
|
|
132
|
+
"'Mow lawn'",
|
|
133
|
+
"'this is valid, right?'",
|
|
134
|
+
'id42',
|
|
135
|
+
),
|
|
136
|
+
])
|
|
137
|
+
|
|
138
|
+
expect(result).toEqual({ id42: { status: CheckStatus.Passed } })
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
it('should be allow mutations in unrelated files', async () => {
|
|
142
|
+
const result = await sut.check([
|
|
143
|
+
createMutant('not-type-checked.js', 'bar', 'baz', 'id1'),
|
|
144
|
+
])
|
|
145
|
+
|
|
146
|
+
expect(result).toEqual({ id1: { status: CheckStatus.Passed } })
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
it('should allow unused local variables (override options)', async () => {
|
|
150
|
+
const mutant = createMutant(
|
|
151
|
+
'todo.ts',
|
|
152
|
+
'TodoList.allTodos.push(newItem)',
|
|
153
|
+
'42',
|
|
154
|
+
'id45',
|
|
155
|
+
)
|
|
156
|
+
const actual = await sut.check([mutant])
|
|
157
|
+
expect(actual).toEqual({ id45: { status: CheckStatus.Passed } })
|
|
158
|
+
})
|
|
159
|
+
|
|
160
|
+
it('should be able invalidate 2 mutants that do result in a compile errors', async () => {
|
|
161
|
+
const mutant = createMutant(
|
|
162
|
+
'todo.ts',
|
|
163
|
+
'TodoList.allTodos.push(newItem)',
|
|
164
|
+
'"This should not be a string 🙄"',
|
|
165
|
+
'mutId',
|
|
166
|
+
)
|
|
167
|
+
const mutant2 = createMutant(
|
|
168
|
+
'counter.ts',
|
|
169
|
+
'return this.currentNumber',
|
|
170
|
+
'return "This should not return a string 🙄"',
|
|
171
|
+
'mutId2',
|
|
172
|
+
)
|
|
173
|
+
const actual = await sut.check([mutant, mutant2])
|
|
174
|
+
expect(actual['mutId']!.status).toBe(CheckStatus.CompileError)
|
|
175
|
+
expect(actual['mutId2']!.status).toBe(CheckStatus.CompileError)
|
|
176
|
+
expect((actual['mutId'] as FailedCheckResult).reason).toContain(
|
|
177
|
+
'todo.ts(15,9): error TS2322',
|
|
178
|
+
)
|
|
179
|
+
expect((actual['mutId2'] as FailedCheckResult).reason).toContain(
|
|
180
|
+
'counter.ts(7,5): error TS2322',
|
|
181
|
+
)
|
|
182
|
+
})
|
|
183
|
+
|
|
184
|
+
it('should be able invalidate 2 mutants that do result in a compile error in file above', async () => {
|
|
185
|
+
const mutant = createMutant(
|
|
186
|
+
'errorInFileAbove2Mutants/todo.ts',
|
|
187
|
+
'TodoList.allTodos.push(newItem)',
|
|
188
|
+
'"This should not be a string 🙄"',
|
|
189
|
+
'mutId',
|
|
190
|
+
)
|
|
191
|
+
const mutant2 = createMutant(
|
|
192
|
+
'errorInFileAbove2Mutants/counter.ts',
|
|
193
|
+
'return (this.currentNumber += numberToIncrementBy)',
|
|
194
|
+
'return "This should not return a string 🙄"',
|
|
195
|
+
'mutId2',
|
|
196
|
+
)
|
|
197
|
+
const actual = await sut.check([mutant, mutant2])
|
|
198
|
+
expect(actual['mutId']!.status).toBe(CheckStatus.CompileError)
|
|
199
|
+
expect(actual['mutId2']!.status).toBe(CheckStatus.CompileError)
|
|
200
|
+
expect((actual['mutId'] as FailedCheckResult).reason).toContain(
|
|
201
|
+
'todo.ts(15,9): error TS2322',
|
|
202
|
+
)
|
|
203
|
+
expect((actual['mutId2'] as FailedCheckResult).reason).toContain(
|
|
204
|
+
'errorInFileAbove2Mutants/todo-counter.ts(7,7): error TS2322',
|
|
205
|
+
)
|
|
206
|
+
})
|
|
207
|
+
})
|
|
208
|
+
|
|
209
|
+
const fileContents: Record<string, string> = Object.freeze({
|
|
210
|
+
['errorInFileAbove2Mutants/todo.ts']: fs.readFileSync(
|
|
211
|
+
resolveTestResource('src', 'errorInFileAbove2Mutants', 'todo.ts'),
|
|
212
|
+
'utf8',
|
|
213
|
+
),
|
|
214
|
+
['errorInFileAbove2Mutants/counter.ts']: fs.readFileSync(
|
|
215
|
+
resolveTestResource('src', 'errorInFileAbove2Mutants', 'counter.ts'),
|
|
216
|
+
'utf8',
|
|
217
|
+
),
|
|
218
|
+
['todo.ts']: fs.readFileSync(resolveTestResource('src', 'todo.ts'), 'utf8'),
|
|
219
|
+
['counter.ts']: fs.readFileSync(
|
|
220
|
+
resolveTestResource('src', 'counter.ts'),
|
|
221
|
+
'utf8',
|
|
222
|
+
),
|
|
223
|
+
['todo.spec.ts']: fs.readFileSync(
|
|
224
|
+
resolveTestResource('src', 'todo.spec.ts'),
|
|
225
|
+
'utf8',
|
|
226
|
+
),
|
|
227
|
+
['not-type-checked.js']: fs.readFileSync(
|
|
228
|
+
resolveTestResource('src', 'not-type-checked.js'),
|
|
229
|
+
'utf8',
|
|
230
|
+
),
|
|
231
|
+
})
|
|
232
|
+
|
|
233
|
+
function createMutant(
|
|
234
|
+
fileName:
|
|
235
|
+
| 'counter.ts'
|
|
236
|
+
| 'errorInFileAbove2Mutants/counter.ts'
|
|
237
|
+
| 'errorInFileAbove2Mutants/todo.ts'
|
|
238
|
+
| 'not-type-checked.js'
|
|
239
|
+
| 'todo.spec.ts'
|
|
240
|
+
| 'todo.ts',
|
|
241
|
+
findText: string,
|
|
242
|
+
replacement: string,
|
|
243
|
+
id = '42',
|
|
244
|
+
offset = 0,
|
|
245
|
+
): Mutant {
|
|
246
|
+
const lines = fileContents[fileName]!.split('\n')
|
|
247
|
+
const lineNumber = lines.findIndex((line) => line.includes(findText))
|
|
248
|
+
if (lineNumber === -1) {
|
|
249
|
+
throw new Error(`Cannot find ${findText} in ${fileName}`)
|
|
250
|
+
}
|
|
251
|
+
const textColumn = lines[lineNumber]!.indexOf(findText)
|
|
252
|
+
const location: Location = {
|
|
253
|
+
start: { line: lineNumber, column: textColumn + offset },
|
|
254
|
+
end: { line: lineNumber, column: textColumn + findText.length },
|
|
255
|
+
}
|
|
256
|
+
return {
|
|
257
|
+
id,
|
|
258
|
+
fileName: resolveTestResource('src', fileName),
|
|
259
|
+
mutatorName: 'foo-mutator',
|
|
260
|
+
location,
|
|
261
|
+
replacement,
|
|
262
|
+
}
|
|
263
|
+
}
|