Skip to content

Commit

Permalink
Merge branch '3.x' into feat/debug-highlight
Browse files Browse the repository at this point in the history
  • Loading branch information
DavertMik committed Jan 7, 2025
2 parents 925467b + d1c91c6 commit 4381716
Show file tree
Hide file tree
Showing 22 changed files with 372 additions and 429 deletions.
3 changes: 1 addition & 2 deletions eslint.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -16,15 +16,14 @@ export default [
{
ignores: ['test/data/output', 'lib/css2xpath/*'],
},
...compat.extends('airbnb-base'),
{
languageOptions: {
globals: {
...globals.node,
},

ecmaVersion: 2020,
sourceType: 'commonjs',
sourceType: 'module',
},

rules: {
Expand Down
138 changes: 69 additions & 69 deletions lib/heal.js
Original file line number Diff line number Diff line change
@@ -1,170 +1,170 @@
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');
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;
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;
this.recipes = {}
this.fixes = []
this.prepareFns = []
this.contextName = null
this.numHealed = 0
}

addRecipe(name, opts = {}) {
if (!opts.priority) opts.priority = 0;
if (!opts.priority) opts.priority = 0

if (!opts.fn) throw new Error(`Recipe ${name} should have a function 'fn' to execute`);
if (!opts.fn) throw new Error(`Recipe ${name} should have a function 'fn' to execute`)

this.recipes[name] = opts;
this.recipes[name] = opts
}

connectToEvents() {
event.dispatcher.on(event.suite.before, suite => {
this.contextName = suite.title;
});
this.contextName = suite.title
})

event.dispatcher.on(event.test.started, test => {
this.contextName = test.fullTitle();
});
this.contextName = test.fullTitle()
})

event.dispatcher.on(event.test.finished, () => {
this.contextName = null;
});
this.contextName = null
})
}

hasCorrespondingRecipes(step) {
return matchRecipes(this.recipes, this.contextName).filter(r => !r.steps || r.steps.includes(step.name)).length > 0;
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);
const suggestions = []
const recipes = matchRecipes(this.recipes, this.contextName)

debug('Recipes', recipes);
debug('Recipes', recipes)

const currentOutputLevel = output.level();
output.level(0);
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 (!prepareFn) continue

if (context[property]) continue;
context[property] = await prepareFn(Container.support());
if (context[property]) continue
context[property] = await prepareFn(Container.support())
}

output.level(currentOutputLevel);
output.level(currentOutputLevel)

for (const recipe of recipes) {
let snippets = await recipe.fn(context);
if (!Array.isArray(snippets)) snippets = [snippets];
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));
return suggestions.filter(s => !isBlank(s.snippets))
}

async healStep(failedStep, error, failureContext = {}) {
output.debug(`Trying to heal ${failedStep.toCode()} step`);
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);
const suggestions = await this.getCodeSuggestions(failureContext)

if (suggestions.length === 0) {
debug('No healing suggestions found');
throw error;
debug('No healing suggestions found')
throw error
}

output.debug(`Received ${suggestions.length} suggestion${suggestions.length === 1 ? '' : 's'}`);
output.debug(`Received ${suggestions.length} suggestion${suggestions.length === 1 ? '' : 's'}`)

debug(suggestions);
debug(suggestions)

for (const suggestion of suggestions) {
for (const codeSnippet of suggestion.snippets) {
try {
debug('Executing', codeSnippet);
debug('Executing', codeSnippet)
recorder.catch(e => {
debug(e);
});
debug(e)
})

if (typeof codeSnippet === 'string') {
const I = Container.support('I');
await eval(codeSnippet); // eslint-disable-line
const I = Container.support('I')
await eval(codeSnippet)
} else if (typeof codeSnippet === 'function') {
await codeSnippet(Container.support());
await codeSnippet(Container.support())
}

this.fixes.push({
recipe: suggestion.name,
test: failureContext?.test,
step: failedStep,
snippet: codeSnippet,
});
})

recorder.add('healed', () => output.print(colors.bold.green(` Code healed successfully by ${suggestion.name}`), colors.gray('(no errors thrown)')));
this.numHealed++;
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;
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
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);
output.debug(`Couldn't heal the code for ${failedStep.toCode()}`)
recorder.throw(error)
}

static setDefaultHealers() {
require('./template/heal');
require('./template/heal')
}
}

const heal = new Heal();
const heal = new Heal()

module.exports = 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;
recipe.name = name
return recipe
})
.filter(r => !!r.fn);
.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() === '');
return value == null || (Array.isArray(value) && value.length === 0) || (typeof value === 'object' && Object.keys(value).length === 0) || (typeof value === 'string' && value.trim() === '')
}
Loading

0 comments on commit 4381716

Please sign in to comment.