Skip to content

Commit d3dced1

Browse files
authored
fix(cli): stale process (#4367)
1 parent 37af1b2 commit d3dced1

File tree

12 files changed

+202
-92
lines changed

12 files changed

+202
-92
lines changed

lib/cli.js

+15-11
Original file line numberDiff line numberDiff line change
@@ -186,20 +186,24 @@ class Cli extends Base {
186186
}
187187
}
188188

189-
let stack = err.stack ? err.stack.split('\n') : [];
190-
if (stack[0] && stack[0].includes(err.message)) {
191-
stack.shift();
192-
}
189+
try {
190+
let stack = err.stack ? err.stack.split('\n') : [];
191+
if (stack[0] && stack[0].includes(err.message)) {
192+
stack.shift();
193+
}
193194

194-
if (output.level() < 3) {
195-
stack = stack.slice(0, 3);
196-
}
195+
if (output.level() < 3) {
196+
stack = stack.slice(0, 3);
197+
}
197198

198-
err.stack = `${stack.join('\n')}\n\n${output.colors.blue(log)}`;
199+
err.stack = `${stack.join('\n')}\n\n${output.colors.blue(log)}`;
199200

200-
// clone err object so stack trace adjustments won't affect test other reports
201-
test.err = err;
202-
return test;
201+
// clone err object so stack trace adjustments won't affect test other reports
202+
test.err = err;
203+
return test;
204+
} catch (e) {
205+
throw Error(e);
206+
}
203207
});
204208

205209
const originalLog = Base.consoleLog;

lib/command/gherkin/init.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ module.exports = function (genPath) {
7070
}
7171

7272
config.gherkin = {
73-
features: './features/*.feature',
73+
features: "./features/*.feature",
7474
steps: [`./step_definitions/steps.${extension}`],
7575
};
7676

lib/plugin/retryTo.js

+33-25
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
const recorder = require('../recorder');
2-
const store = require('../store');
32
const { debug } = require('../output');
43

54
const defaultConfig = {
@@ -73,49 +72,58 @@ const defaultConfig = {
7372
* const retryTo = codeceptjs.container.plugins('retryTo');
7473
* ```
7574
*
76-
*/
75+
*/
7776
module.exports = function (config) {
7877
config = Object.assign(defaultConfig, config);
78+
function retryTo(callback, maxTries, pollInterval = config.pollInterval) {
79+
return new Promise((done, reject) => {
80+
let tries = 1;
7981

80-
if (config.registerGlobal) {
81-
global.retryTo = retryTo;
82-
}
83-
return retryTo;
82+
function handleRetryException(err) {
83+
recorder.throw(err);
84+
reject(err);
85+
}
8486

85-
function retryTo(callback, maxTries, pollInterval = undefined) {
86-
let tries = 1;
87-
if (!pollInterval) pollInterval = config.pollInterval;
88-
89-
let err = null;
90-
91-
return new Promise((done) => {
9287
const tryBlock = async () => {
88+
tries++;
9389
recorder.session.start(`retryTo ${tries}`);
94-
await callback(tries);
90+
try {
91+
await callback(tries);
92+
} catch (err) {
93+
handleRetryException(err);
94+
}
95+
96+
// Call done if no errors
9597
recorder.add(() => {
9698
recorder.session.restore(`retryTo ${tries}`);
9799
done(null);
98100
});
99-
recorder.session.catch((e) => {
100-
err = e;
101+
102+
// Catch errors and retry
103+
recorder.session.catch((err) => {
101104
recorder.session.restore(`retryTo ${tries}`);
102-
tries++;
103105
if (tries <= maxTries) {
104106
debug(`Error ${err}... Retrying`);
105-
err = null;
106-
107-
recorder.add(`retryTo ${tries}`, () => setTimeout(tryBlock, pollInterval));
107+
recorder.add(`retryTo ${tries}`, () =>
108+
setTimeout(tryBlock, pollInterval)
109+
);
108110
} else {
109-
done(null);
111+
// if maxTries reached
112+
handleRetryException(err);
110113
}
111114
});
112115
};
113116

114-
recorder.add('retryTo', async () => {
115-
tryBlock();
117+
recorder.add('retryTo', tryBlock).catch(err => {
118+
console.error('An error occurred:', err);
119+
done(null);
116120
});
117-
}).then(() => {
118-
if (err) recorder.throw(err);
119121
});
120122
}
123+
124+
if (config.registerGlobal) {
125+
global.retryTo = retryTo;
126+
}
127+
128+
return retryTo;
121129
};

lib/scenario.js

+52-49
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,16 @@ const injectHook = function (inject, suite) {
1818
return recorder.promise();
1919
};
2020

21+
function makeDoneCallableOnce(done) {
22+
let called = false;
23+
return function (err) {
24+
if (called) {
25+
return;
26+
}
27+
called = true;
28+
return done(err);
29+
};
30+
}
2131
/**
2232
* Wraps test function, injects support objects from container,
2333
* starts promise chain with recorder, performs before/after hooks
@@ -34,56 +44,44 @@ module.exports.test = (test) => {
3444
test.async = true;
3545

3646
test.fn = function (done) {
47+
const doneFn = makeDoneCallableOnce(done);
3748
recorder.errHandler((err) => {
3849
recorder.session.start('teardown');
3950
recorder.cleanAsyncErr();
40-
if (test.throws) { // check that test should actually fail
51+
if (test.throws) {
52+
// check that test should actually fail
4153
try {
4254
assertThrown(err, test.throws);
4355
event.emit(event.test.passed, test);
4456
event.emit(event.test.finished, test);
45-
recorder.add(() => done());
57+
recorder.add(doneFn);
4658
return;
4759
} catch (newErr) {
4860
err = newErr;
4961
}
5062
}
5163
event.emit(event.test.failed, test, err);
5264
event.emit(event.test.finished, test);
53-
recorder.add(() => done(err));
65+
recorder.add(() => doneFn(err));
5466
});
5567

5668
if (isAsyncFunction(testFn)) {
5769
event.emit(event.test.started, test);
58-
59-
const catchError = e => {
60-
recorder.throw(e);
61-
recorder.catch((e) => {
62-
const err = (recorder.getAsyncErr() === null) ? e : recorder.getAsyncErr();
63-
recorder.session.start('teardown');
64-
recorder.cleanAsyncErr();
65-
event.emit(event.test.failed, test, err);
66-
event.emit(event.test.finished, test);
67-
recorder.add(() => done(err));
70+
testFn
71+
.call(test, getInjectedArguments(testFn, test))
72+
.then(() => {
73+
recorder.add('fire test.passed', () => {
74+
event.emit(event.test.passed, test);
75+
event.emit(event.test.finished, test);
76+
});
77+
recorder.add('finish test', doneFn);
78+
})
79+
.catch((err) => {
80+
recorder.throw(err);
81+
})
82+
.finally(() => {
83+
recorder.catch();
6884
});
69-
};
70-
71-
let injectedArguments;
72-
try {
73-
injectedArguments = getInjectedArguments(testFn, test);
74-
} catch (e) {
75-
catchError(e);
76-
return;
77-
}
78-
79-
testFn.call(test, injectedArguments).then(() => {
80-
recorder.add('fire test.passed', () => {
81-
event.emit(event.test.passed, test);
82-
event.emit(event.test.finished, test);
83-
});
84-
recorder.add('finish test', () => done());
85-
recorder.catch();
86-
}).catch(catchError);
8785
return;
8886
}
8987

@@ -97,7 +95,7 @@ module.exports.test = (test) => {
9795
event.emit(event.test.passed, test);
9896
event.emit(event.test.finished, test);
9997
});
100-
recorder.add('finish test', () => done());
98+
recorder.add('finish test', doneFn);
10199
recorder.catch();
102100
}
103101
};
@@ -109,13 +107,14 @@ module.exports.test = (test) => {
109107
*/
110108
module.exports.injected = function (fn, suite, hookName) {
111109
return function (done) {
110+
const doneFn = makeDoneCallableOnce(done);
112111
const errHandler = (err) => {
113112
recorder.session.start('teardown');
114113
recorder.cleanAsyncErr();
115114
event.emit(event.test.failed, suite, err);
116115
if (hookName === 'after') event.emit(event.test.after, suite);
117116
if (hookName === 'afterSuite') event.emit(event.suite.after, suite);
118-
recorder.add(() => done(err));
117+
recorder.add(() => doneFn(err));
119118
};
120119

121120
recorder.errHandler((err) => {
@@ -137,28 +136,32 @@ module.exports.injected = function (fn, suite, hookName) {
137136
const opts = suite.opts || {};
138137
const retries = opts[`retry${ucfirst(hookName)}`] || 0;
139138

140-
promiseRetry(async (retry, number) => {
141-
try {
142-
recorder.startUnlessRunning();
143-
await fn.call(this, getInjectedArguments(fn));
144-
await recorder.promise().catch(err => retry(err));
145-
} catch (err) {
146-
retry(err);
147-
} finally {
148-
if (number < retries) {
149-
recorder.stop();
150-
recorder.start();
139+
promiseRetry(
140+
async (retry, number) => {
141+
try {
142+
recorder.startUnlessRunning();
143+
await fn.call(this, getInjectedArguments(fn));
144+
await recorder.promise().catch((err) => retry(err));
145+
} catch (err) {
146+
retry(err);
147+
} finally {
148+
if (number < retries) {
149+
recorder.stop();
150+
recorder.start();
151+
}
151152
}
152-
}
153-
}, { retries })
153+
},
154+
{ retries },
155+
)
154156
.then(() => {
155157
recorder.add('fire hook.passed', () => event.emit(event.hook.passed, suite));
156-
recorder.add(`finish ${hookName} hook`, () => done());
158+
recorder.add(`finish ${hookName} hook`, doneFn);
157159
recorder.catch();
158-
}).catch((e) => {
160+
})
161+
.catch((e) => {
159162
recorder.throw(e);
160163
recorder.catch((e) => {
161-
const err = (recorder.getAsyncErr() === null) ? e : recorder.getAsyncErr();
164+
const err = recorder.getAsyncErr() === null ? e : recorder.getAsyncErr();
162165
errHandler(err);
163166
});
164167
recorder.add('fire hook.failed', () => event.emit(event.hook.failed, suite, e));

test/acceptance/retryTo_test.js

+20
Original file line numberDiff line numberDiff line change
@@ -14,3 +14,23 @@ Scenario('retryTo works with non await steps @plugin', async () => {
1414
if (tryNum < 3) I.waitForVisible('.nothing', 1);
1515
}, 4);
1616
});
17+
18+
Scenario('Should be succeed', async ({ I }) => {
19+
I.amOnPage('http://example.org');
20+
I.waitForVisible('.nothing', 1); // should fail here but it won't terminate
21+
await retryTo((tryNum) => {
22+
I.see('.doesNotMatter');
23+
}, 10);
24+
});
25+
26+
Scenario('Should fail after reached max retries', async () => {
27+
await retryTo(() => {
28+
throw new Error('Custom pluginRetryTo Error');
29+
}, 3);
30+
});
31+
32+
Scenario('Should succeed at the third attempt @plugin', async () => {
33+
await retryTo(async (tryNum) => {
34+
if (tryNum < 2) throw new Error('Custom pluginRetryTo Error');
35+
}, 3);
36+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
exports.config = {
2+
tests: './test.scenario-stale.js',
3+
timeout: 10000,
4+
retry: 2,
5+
output: './output',
6+
include: {},
7+
bootstrap: false,
8+
mocha: {},
9+
name: 'sandbox',
10+
};
+22
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
Feature('Scenario should not be staling');
2+
3+
const SHOULD_NOT_STALE = 'should not stale scenario error';
4+
5+
Scenario('Rejected promise should not stale the process', async () => {
6+
await new Promise((_resolve, reject) => setTimeout(reject(new Error(SHOULD_NOT_STALE)), 500));
7+
});
8+
9+
Scenario('Should handle throw inside synchronous and terminate gracefully', () => {
10+
throw new Error(SHOULD_NOT_STALE);
11+
});
12+
Scenario('Should handle throw inside async and terminate gracefully', async () => {
13+
throw new Error(SHOULD_NOT_STALE);
14+
});
15+
16+
Scenario('Should throw, retry and keep failing', async () => {
17+
setTimeout(() => {
18+
throw new Error(SHOULD_NOT_STALE);
19+
}, 500);
20+
await new Promise((resolve) => setTimeout(resolve, 300));
21+
throw new Error(SHOULD_NOT_STALE);
22+
}).retry(2);

test/plugin/plugin_test.js

+22
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,15 @@ describe('CodeceptJS plugin', function () {
3232
});
3333
});
3434

35+
it('should failed before the retryTo instruction', (done) => {
36+
exec(`${config_run_config('codecept.Playwright.retryTo.js', 'Should be succeed')} --verbose`, (err, stdout) => {
37+
expect(stdout).toContain('locator.waitFor: Timeout 1000ms exceeded.'),
38+
expect(stdout).toContain('[1] Error | Error: element (.nothing) still not visible after 1 sec'),
39+
expect(err).toBeTruthy();
40+
done();
41+
});
42+
});
43+
3544
it('should generate the coverage report', (done) => {
3645
exec(`${config_run_config('codecept.Playwright.coverage.js', '@coverage')} --debug`, (err, stdout) => {
3746
const lines = stdout.split('\n');
@@ -61,4 +70,17 @@ describe('CodeceptJS plugin', function () {
6170
done();
6271
});
6372
});
73+
74+
it('should retry to failure', (done) => {
75+
exec(
76+
`${config_run_config('codecept.Playwright.retryTo.js', 'Should fail after reached max retries')} --verbose`, (err, stdout) => {
77+
const lines = stdout.split('\n');
78+
expect(lines).toEqual(
79+
expect.arrayContaining([expect.stringContaining('Custom pluginRetryTo Error')])
80+
);
81+
expect(err).toBeTruthy();
82+
done();
83+
}
84+
);
85+
});
6486
});

0 commit comments

Comments
 (0)