codeceptjs
Version:
Supercharged End 2 End Testing Framework for NodeJS
181 lines (146 loc) • 5.04 kB
JavaScript
const debug = require('debug')('codeceptjs:heal')
const colors = require('chalk')
const Container = require('./container')
const recorder = require('./recorder')
const output = require('./output')
const event = require('./event')
/**
* @class
*/
class Heal {
constructor() {
this.recipes = {}
this.fixes = []
this.prepareFns = []
this.contextName = null
this.numHealed = 0
}
clear() {
this.recipes = {}
this.fixes = []
this.prepareFns = []
this.contextName = null
this.numHealed = 0
}
addRecipe(name, opts = {}) {
if (!opts.priority) opts.priority = 0
if (!opts.fn) throw new Error(`Recipe ${name} should have a function 'fn' to execute`)
this.recipes[name] = opts
}
connectToEvents() {
event.dispatcher.on(event.suite.before, suite => {
this.contextName = suite.title
})
event.dispatcher.on(event.test.started, test => {
this.contextName = test.fullTitle()
})
event.dispatcher.on(event.test.finished, () => {
this.contextName = null
})
}
hasCorrespondingRecipes(step) {
return matchRecipes(this.recipes, this.contextName).filter(r => !r.steps || r.steps.includes(step.name)).length > 0
}
async getCodeSuggestions(context) {
const suggestions = []
const recipes = matchRecipes(this.recipes, this.contextName)
debug('Recipes', recipes)
const currentOutputLevel = output.level()
output.level(0)
for (const [property, prepareFn] of Object.entries(
recipes
.map(r => r.prepare)
.filter(p => !!p)
.reduce((acc, obj) => ({ ...acc, ...obj }), {}),
)) {
if (!prepareFn) continue
if (context[property]) continue
context[property] = await prepareFn(Container.support())
}
output.level(currentOutputLevel)
for (const recipe of recipes) {
let snippets = await recipe.fn(context)
if (!Array.isArray(snippets)) snippets = [snippets]
suggestions.push({
name: recipe.name,
snippets,
})
}
return suggestions.filter(s => !isBlank(s.snippets))
}
async healStep(failedStep, error, failureContext = {}) {
output.debug(`Trying to heal ${failedStep.toCode()} step`)
Object.assign(failureContext, {
error,
step: failedStep,
prevSteps: failureContext?.test?.steps?.slice(0, -1) || [],
})
const suggestions = await this.getCodeSuggestions(failureContext)
if (suggestions.length === 0) {
debug('No healing suggestions found')
throw error
}
output.debug(`Received ${suggestions.length} suggestion${suggestions.length === 1 ? '' : 's'}`)
debug(suggestions)
for (const suggestion of suggestions) {
for (const codeSnippet of suggestion.snippets) {
try {
debug('Executing', codeSnippet)
recorder.catch(e => {
debug(e)
})
if (typeof codeSnippet === 'string') {
const I = Container.support('I')
await eval(codeSnippet)
} else if (typeof codeSnippet === 'function') {
await codeSnippet(Container.support())
}
this.fixes.push({
recipe: suggestion.name,
test: failureContext?.test,
step: failedStep,
snippet: codeSnippet,
})
if (failureContext?.test) {
const test = failureContext.test
let note = `This test was healed by '${suggestion.name}'`
note += `\n\nReplace the failed code:\n\n`
note += colors.red(`- ${failedStep.toCode()}\n`)
note += colors.green(`+ ${codeSnippet}\n`)
test.addNote('heal', note)
test.meta.healed = true
}
recorder.add('healed', () => output.print(colors.bold.green(` Code healed successfully by ${suggestion.name}`), colors.gray('(no errors thrown)')))
this.numHealed++
// recorder.session.restore();
return
} catch (err) {
debug('Failed to execute code', err)
recorder.ignoreErr(err) // healing did not help
recorder.catchWithoutStop(err)
await recorder.promise() // wait for all promises to resolve
}
}
}
output.debug(`Couldn't heal the code for ${failedStep.toCode()}`)
recorder.throw(error)
}
static setDefaultHealers() {
require('./template/heal')
}
}
const heal = new Heal()
module.exports = heal
function matchRecipes(recipes, contextName) {
return Object.entries(recipes)
.filter(([, recipe]) => !contextName || !recipe.grep || new RegExp(recipe.grep).test(contextName))
.sort(([, a], [, b]) => a.priority - b.priority)
.map(([name, recipe]) => {
recipe.name = name
return recipe
})
.filter(r => !!r.fn)
}
function isBlank(value) {
return value == null || (Array.isArray(value) && value.length === 0) || (typeof value === 'object' && Object.keys(value).length === 0) || (typeof value === 'string' && value.trim() === '')
}