Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add option to include contextual information in code fragments #39

Open
wants to merge 7 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
119 changes: 88 additions & 31 deletions analyzer.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,20 @@ const path = require('path');
let workDir;
const invalidKeys = ['And', 'But'];

const getLocation = scenario => (scenario.tags.length ? scenario.tags[0].location.line - 1 : scenario.location.line - 1);
const getLocation = (message) => {
if (message.tags && message.tags.length) {
return message.tags[0].location.line - 1;
}
return message.location.line - 1;
};

const getLocations = ({ feature, scenario, rule, background }) => {
if (feature) return [getLocation(feature), ...feature.children.flatMap(getLocations) ];
if (scenario) return [ getLocation(scenario) ];
if (rule) return [getLocation(rule), ...rule.children.flatMap(getLocations)];
if (background) return [ getLocation(background) ];
return [];
};

const getTitle = scenario => {
let { name } = scenario;
Expand All @@ -24,45 +37,85 @@ const getTitle = scenario => {
return name;
};

const getScenarioCode = (source, feature, file) => {
const getScenarioCode = (source, feature, file, {
includeFeatureCode,
includeRuleCode,
includeBackgroundCode,
}) => {
const sourceArray = source.split('\n');
const fileName = path.relative(workDir, file);
const scenarios = [];

for (let i = 0; i < feature.children.length; i += 1) {
const { scenario } = feature.children[i];
if (scenario) {
if (!scenario.name) {
console.log(chalk.red('Title of scenario cannot be empty, skipping this'));
const [featureStart, ...startLocations] = getLocations({ feature });
const [featureEnd, ...endLocations] = [...startLocations, sourceArray.length];
const context = includeFeatureCode ? sourceArray.slice(featureStart, featureEnd) : [];

let inRule = false;

const handleScenario = (scenario) => {
if (!scenario.name) {
console.log(chalk.red('Title of scenario cannot be empty, skipping this'));
} else {
console.log(inRule ? ' - ' : ' - ', scenario.name);
}
const steps = [];
let previousValidStep = '';
const scenarioJson = { name: scenario.name, file: fileName };
const start = startLocations.shift();
const end = endLocations.shift();
for (const step of scenario.steps) {
let keyword = step.keyword.trim();
if (invalidKeys.includes(keyword)) {
keyword = previousValidStep;
} else {
console.log(' - ', scenario.name);
previousValidStep = keyword;
}
const steps = [];
let previousValidStep = '';
const scenarioJson = { name: scenario.name, file: fileName };
const start = getLocation(scenario);
const end = ((i === feature.children.length - 1) ? sourceArray.length : getLocation(feature.children[i + 1].scenario));
for (const step of scenario.steps) {
let keyword = step.keyword.trim();
if (invalidKeys.includes(keyword)) {
keyword = previousValidStep;
} else {
previousValidStep = keyword;
}
steps.push({ title: step.text, keyword });
}
scenarioJson.line = start;
scenarioJson.tags = scenario.tags.map(t => t.name.slice(1));
scenarioJson.code = sourceArray.slice(start, end).join('\n');
scenarioJson.steps = steps;
scenarios.push(scenarioJson);
steps.push({ title: step.text, keyword });
}
scenarioJson.line = start;
scenarioJson.tags = scenario.tags.map(t => t.name.slice(1));
scenarioJson.code = context.concat(sourceArray.slice(start, end)).join('\n');
scenarioJson.steps = steps;
scenarios.push(scenarioJson);
};

const handleRule = (rule) => {
console.log(' - ', rule.name);
const oldContextLength = context.length;
const start = startLocations.shift();
const end = endLocations.shift();

if (includeRuleCode) {
context.push(...sourceArray.slice(start, end));
}

inRule = true;
rule.children.forEach(handleChild);
inRule = false;

context.splice(oldContextLength);
};

const handleBackground = (background) => {
const start = startLocations.shift();
const end = endLocations.shift();
if (includeBackgroundCode) {
context.push(...sourceArray.slice(start, end));
}
};

const handleChild = ({ scenario, rule, background }) => {
if (scenario) handleScenario(scenario);
if (rule) handleRule(rule);
if (background) handleBackground(background);
}

feature.children.forEach(handleChild);

return scenarios;
};

const parseFile = file => new Promise((resolve, reject) => {
const parseFile = (file, scenarioCodeOptions) => new Promise((resolve, reject) => {
try {
const options = {
includeSource: true,
Expand Down Expand Up @@ -91,7 +144,7 @@ const parseFile = file => new Promise((resolve, reject) => {
}
featureData.line = getLocation(data[1].gherkinDocument.feature) + 1;
featureData.tags = data[1].gherkinDocument.feature.tags.map(t => t.name.slice(1));
featureData.scenario = getScenarioCode(data[0].source.data, data[1].gherkinDocument.feature, file);
featureData.scenario = getScenarioCode(data[0].source.data, data[1].gherkinDocument.feature, file, scenarioCodeOptions);
} else {
featureData.error = `${fileName} : ${data[1].attachment.data}`;
console.log(chalk.red(`Wrong format, So skipping this: ${data[1].attachment.data}`));
Expand All @@ -109,8 +162,12 @@ const parseFile = file => new Promise((resolve, reject) => {
*
* @param {String} filePattern
* @param {String} dir
* @param {Object} scenarioCodeOptions
* @param {boolean} scenarioCodeOptions.includeFeatureCode
* @param {boolean} scenarioCodeOptions.includeRuleCode
* @param {boolean} scenarioCodeOptions.includeBackgroundCode
*/
const analyzeFeatureFiles = (filePattern, dir = '.') => {
const analyzeFeatureFiles = (filePattern, dir = '.', scenarioCodeOptions = {}) => {
workDir = dir;

console.log('\n 🗄️ Parsing files\n');
Expand All @@ -120,7 +177,7 @@ const analyzeFeatureFiles = (filePattern, dir = '.') => {
const promiseArray = [];
glob(pattern, (er, files) => {
for (const file of files) {
const data = parseFile(file);
const data = parseFile(file, scenarioCodeOptions);
promiseArray.push(data);
}

Expand Down
11 changes: 9 additions & 2 deletions bin/check.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,10 +32,17 @@ program
.option('--create', 'Create tests and suites for missing IDs')
.option('--no-empty', 'Remove empty suites after import')
.option('--keep-structure', 'Prefer structure of source code over structure in Testomat.io')
.option('--no-detached', 'Don\t mark all unmatched tests as detached')
.option('--no-detached', 'Don\'t mark all unmatched tests as detached')
.option('--include-features', 'Add description of feature to scenario code')
.option('--include-rules', 'Add description of parent rule sections to scenario code')
.option('--include-backgrounds', 'Add description and steps of relevant background sections to scenario code')
.action(async (filesArg, opts) => {
const isPattern = checkPattern(filesArg);
const features = await analyze(filesArg || '**/*.feature', opts.dir || process.cwd());
const features = await analyze(filesArg || '**/*.feature', opts.dir || process.cwd(), {
includeFeatureCode: opts.includeFeatures,
includeRuleCode: opts.includeRules,
includeBackgroundCode: opts.includeBackgrounds,
});
if (opts.cleanIds || opts.unsafeCleanIds) {
let idMap = {};
if (apiKey) {
Expand Down
86 changes: 86 additions & 0 deletions example/features/rules.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
Feature: A feature with multiple rules
Description of the feature

Background:
Given all rules have something

Rule: Rule 1
Description of first rule

Background:
Given the first rule has something

Scenario: Scenario 1.1
Description of first scenario

Given I have something
When I do something
Then something happens

Scenario: Scenario 1.2
Description of second scenario

Given I have something
When I do something
Then something happens

Scenario: Scenario 1.3
Description of third scenario

Given I have something
When I do something
Then something happens

Rule: Rule 2
Description of second rule

Background:
Given the second rule has something

Scenario: Scenario 2.1
Description of first scenario

Given I have something
When I do something
Then something happens

Scenario: Scenario 2.2
Description of second scenario

Given I have something
When I do something
Then something happens

Scenario: Scenario 2.3
Description of third scenario

Given I have something
When I do something
Then something happens

Rule: Rule 3
Description of third rule

Background:
Given the third rule has something

Scenario: Scenario 3.1
Description of first scenario

Given I have something
When I do something
Then something happens

Scenario: Scenario 3.2
Description of second scenario

Given I have something
When I do something
Then something happens

Scenario: Scenario 3.3
Description of third scenario

Given I have something
When I do something
Then something happens
Loading