Skip to content

Commit

Permalink
test: new test runner without jscodeshift
Browse files Browse the repository at this point in the history
  • Loading branch information
pionxzh committed Jan 14, 2024
1 parent 9e37839 commit 1028e89
Show file tree
Hide file tree
Showing 19 changed files with 271 additions and 270 deletions.
6 changes: 6 additions & 0 deletions eslint.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,12 @@ export default pionxzh(
'pionxzh/consistent-list-newline': 'off',
},
},
{
files: ['packages/unminify/**/*.spec.ts'],
rules: {
'style/indent': ['error', 2],
},
},
{
files: ['examples/**'],
rules: {
Expand Down
4 changes: 3 additions & 1 deletion packages/shared/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
"./array": "./src/array.ts",
"./jscodeshift": "./src/jscodeshift.ts",
"./rule": "./src/rule.ts",
"./runner": "./src/runner.ts",
"./timing": "./src/timing.ts",
"./types": "./src/types.ts"
},
Expand All @@ -28,7 +29,8 @@
"lint:fix": "eslint src --fix"
},
"dependencies": {
"@babel/parser": "^7.23.6"
"@babel/parser": "^7.23.6",
"pathe": "^1.1.1"
},
"devDependencies": {
"@types/jscodeshift": "^0.11.11",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { expect, it } from 'vitest'
import { arraify } from '../arraify'
import { arraify } from '../array'

it('should always return an array', () => {
expect(arraify(1)).toEqual([1])
Expand Down
9 changes: 9 additions & 0 deletions packages/shared/src/array.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,12 @@
export type MaybeArray<T> = T | T[]

export function arraify<T>(value: T | T[]): T[] {
if (Array.isArray(value)) {
return value
}
return [value]
}

export function nonNullable<T>(x: T): x is NonNullable<T> {
return x != null
}
104 changes: 104 additions & 0 deletions packages/shared/src/runner.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import { basename } from 'pathe'
import { arraify } from './array'
import { jscodeshiftWithParser as j, printSourceWithErrorLoc } from './jscodeshift'
import { Timing } from './timing'
import type { MaybeArray } from './array'
import type { TransformationRule } from './rule'
import type { Collection } from 'jscodeshift'

export async function executeTransformationRules<P extends Record<string, any>>(
/** The source code */
source: string,
/** The file path */
filePath: string,
rules: MaybeArray<TransformationRule>,
params: P = {} as any,
) {
const timing = new Timing()
/**
* To minimizes the overhead of parsing and serializing code, we will try to
* keep the code in jscodeshift AST format as long as possible.
*/

let currentSource: string | null = null
let currentRoot: Collection | null = null

const flattenRules = arraify(rules).flatMap((rule) => {
if (rule.type === 'rule-set') return rule.rules
return rule
})

let hasError = false
for (const rule of flattenRules) {
switch (rule.type) {
case 'jscodeshift': {
try {
const stopMeasure = timing.startMeasure(filePath, 'jscodeshift-parse')
currentRoot ??= j(currentSource ?? source)
stopMeasure()
}
catch (err: any) {
console.error(`\nFailed to parse rule ${filePath} with jscodeshift in rule ${rule.id}`, err)
printSourceWithErrorLoc(err, currentSource ?? source)

hasError = true
break
}

const stopMeasure = timing.startMeasure(filePath, rule.id)
// rule execute already handled error
rule.execute({
root: currentRoot,
filename: basename(filePath),
params,
})
stopMeasure()

currentSource = null
break
}
case 'string': {
const stopMeasure1 = timing.startMeasure(filePath, 'jscodeshift-print')
currentSource ??= currentRoot?.toSource() ?? source
stopMeasure1()

try {
const stopMeasure2 = timing.startMeasure(filePath, rule.id)
currentSource = await rule.execute({
source: currentSource,
filename: filePath,
params,
}) ?? currentSource
stopMeasure2()
}
catch (err: any) {
console.error(`\nError running rule ${rule.id} on ${filePath}`, err)

hasError = true
}
currentRoot = null
break
}
default: {
throw new Error(`Unsupported rule type ${rule.type} from ${rule.id}`)
}
}

// stop if there is an error to prevent further damage
if (hasError) break
}

let code = currentSource as string
try {
code ??= currentRoot?.toSource() ?? source
}
catch (err) {
console.error(`\nFailed to print code ${filePath}`, err)
}

return {
path: filePath,
code,
timing,
}
}
105 changes: 44 additions & 61 deletions packages/test-utils/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,85 +1,63 @@
import { mergeTransformationRule } from '@wakaru/shared/rule'
import { runInlineTest } from 'jscodeshift/src/testUtils'
import { it } from 'vitest'
import { executeTransformationRules } from '@wakaru/shared/runner'
import { expect, it } from 'vitest'
import type { TransformationRule } from '@wakaru/shared/rule'

export function defineInlineTestWithOptions(rule: TransformationRule) {
return function inlineTest(
testName: string,
options: any,
input: string,
expectedOutput: string,
) {
it(testName, () => {
runInlineTest(
rule.toJSCodeshiftTransform(),
options,
{ source: input },
expectedOutput,
{ parser: 'babylon' },
)
})
}
interface InlineTest {
(testName: string, input: string, expected: string): void
}

type InlineTest = (
testName: string,
input: string,
expectedOutput: string,
) => void
type TestModifier = 'skip' | 'only' | 'todo' | 'fails'

/**
* Wrapper around `jscodeshift`'s `runInlineTest` that allows for a more
* declarative syntax.
*
* - Supports multiple transforms
* - Supports `skip` and `only` modifiers
*/
export function defineInlineTest(rules: TransformationRule | TransformationRule[]) {
const mergedTransform = Array.isArray(rules)
? mergeTransformationRule(rules.map(rule => rule.name).join(' + '), rules).toJSCodeshiftTransform()
: rules.toJSCodeshiftTransform()

function _inlineTest(
modifier: 'skip' | 'only' | 'todo' | 'fails' | null,
modifier: TestModifier | null,
options: any,
testName: string,
input: string,
expectedOutput: string,
expected: string,
) {
const itFn = modifier ? it[modifier] : it
itFn(testName, () => {
if (!itFn) throw new Error(`Unknown modifier "${modifier}" for test: ${testName}`)

/**
* Capture the stack trace of the test function call so that we can show it in the error
*/
const _error = new Error('_')
const testCallStack = _error.stack ? _error.stack.split('\n')[2] : null

itFn(testName, async () => {
try {
runInlineTest(
mergedTransform,
{},
{ source: input, path: `"testName"` },
expectedOutput,
{ parser: 'babylon' },
)
const output = await executeTransformationRules(input, 'test.js', rules, options)
expect(output.code.trim().replace(/\r\n/g, '\n'))
.toEqual(expected.trim().replace(/\r\n/g, '\n'))
}
catch (err) {
console.error('Error in test:', testCallStack)
/**
* Prevent test utils from showing up in the stack trace.
*/
if (err instanceof Error && err.stack) {
const stack = err.stack
const stacks = stack.split('\n')
const newStacks = stacks.filter((line) => {
const blockList = [
// /@vitest\/runner/,
/test-utils\\src\\index\.ts/,
/jscodeshift\\src\\testUtils\.js/,
]
return !blockList.some(regex => regex.test(line))
})
const stacks = err.stack.split('\n')
const newStacks = [testCallStack, ...stacks]
err.stack = newStacks.join('\n')
}
throw err
}
})
}

const inlineTest = _inlineTest.bind(null, null) as InlineTest & {
const createModifiedFunction = (modifier: TestModifier | null, options: any = {}): InlineTest => {
return Object.assign(
_inlineTest.bind(null, modifier, options),
{
withOptions: (newOptions: any) => {
return createModifiedFunction(modifier, newOptions)
},
},
)
}

const inlineTest = createModifiedFunction(null) as InlineTest & {
/**
* Use `.skip` to skip a test in a given suite. Consider using `.todo` or `.fixme` instead
* if those are more appropriate.
Expand All @@ -98,12 +76,17 @@ export function defineInlineTest(rules: TransformationRule | TransformationRule[
* Use `.fixme` when you are writing a test and **expecting** it to fail.
*/
fixme: InlineTest
/**
* Use `.withOptions` to pass options to the transformation rules.
*/
withOptions: (options: any) => InlineTest
}

inlineTest.skip = _inlineTest.bind(null, 'skip')
inlineTest.only = _inlineTest.bind(null, 'only')
inlineTest.todo = _inlineTest.bind(null, 'todo')
inlineTest.fixme = _inlineTest.bind(null, 'fails')
inlineTest.skip = createModifiedFunction('skip')
inlineTest.only = createModifiedFunction('only')
inlineTest.todo = createModifiedFunction('todo')
inlineTest.fixme = createModifiedFunction('fails')
inlineTest.withOptions = options => _inlineTest.bind(null, null, options)

return inlineTest
}
1 change: 0 additions & 1 deletion packages/unminify/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,6 @@
"@babel/preset-env": "^7.23.3",
"@babel/types": "^7.23.4",
"lebab": "^3.2.4",
"pathe": "^1.1.1",
"picocolors": "^1.0.0",
"prettier": "^2.8.8",
"zod": "^3.22.4"
Expand Down
Loading

0 comments on commit 1028e89

Please sign in to comment.