Skip to content

Commit df11be6

Browse files
authored
Merge pull request #276 from ringcentral/WAT-5697
WAT-5697
2 parents b055e94 + 72f5fda commit df11be6

11 files changed

Lines changed: 610 additions & 16 deletions

File tree

core/api/src/run.ts

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -21,15 +21,7 @@ export async function run(...tests: Array<TestFunction>) {
2121

2222
const api = new TestContext(testParameters.runData);
2323
let passed = false;
24-
let catchedError;
25-
26-
afterRun(async () => {
27-
try {
28-
await api.end();
29-
} catch (err) {
30-
loggerClient.error(err);
31-
}
32-
});
24+
let catchedError: Error | null = null;
3325

3426
try {
3527
await bus.startedTest();
@@ -46,7 +38,16 @@ export async function run(...tests: Array<TestFunction>) {
4638
} catch (error) {
4739
catchedError = restructureError(error as Error);
4840
} finally {
49-
if (passed) {
41+
try {
42+
await api.end();
43+
} catch (error) {
44+
if (!catchedError) {
45+
catchedError = restructureError(error as Error);
46+
passed = false;
47+
}
48+
}
49+
50+
if (passed && !catchedError) {
5051
loggerClient.endStep(testID, 'Test passed');
5152

5253
await bus.finishedTest();

core/api/src/test-context.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -109,8 +109,9 @@ export class TestContext {
109109
}
110110
}
111111

112-
return Promise.all(requests).catch((error) => {
113-
this.logError(error);
112+
return Promise.all(requests).catch(async (error) => {
113+
await this.logWarning(error);
114+
throw error;
114115
});
115116
}
116117

core/api/test/run.spec.ts

Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
1+
/// <reference types="mocha" />
2+
3+
import * as chai from 'chai';
4+
import sinon from 'sinon';
5+
6+
import {loggerClient} from '@testring/logger';
7+
import {TestEvents} from '@testring/types';
8+
9+
import {run} from '../src/run';
10+
import {TestContext} from '../src/test-context';
11+
import {testAPIController} from '../src/test-api-controller';
12+
13+
const TEST_ID = 'test.js';
14+
const LOG_PREFIX = '[logged inside test]';
15+
16+
type Restorable = {
17+
restore: () => void;
18+
};
19+
20+
type RunEvents = {
21+
failedErrors: Error[];
22+
getFinishedCount: () => number;
23+
cleanup: () => void;
24+
};
25+
26+
function prepareTestAPI(): void {
27+
testAPIController.setTestID(TEST_ID);
28+
testAPIController.setTestParameters({runData: {}});
29+
testAPIController.setEnvironmentParameters({});
30+
}
31+
32+
function observeRunEvents(): RunEvents {
33+
const bus = testAPIController.getBus();
34+
let finishedCount = 0;
35+
const failedErrors: Error[] = [];
36+
37+
const finishedHandler = () => {
38+
finishedCount += 1;
39+
};
40+
const failedHandler = (error: Error) => {
41+
failedErrors.push(error);
42+
};
43+
44+
bus.on(TestEvents.finished, finishedHandler);
45+
bus.on(TestEvents.failed, failedHandler);
46+
47+
return {
48+
failedErrors,
49+
getFinishedCount: () => finishedCount,
50+
cleanup: () => {
51+
bus.removeListener(TestEvents.finished, finishedHandler);
52+
bus.removeListener(TestEvents.failed, failedHandler);
53+
},
54+
};
55+
}
56+
57+
function track<T extends Restorable>(
58+
restorables: Restorable[],
59+
restorable: T,
60+
): T {
61+
restorables.push(restorable);
62+
63+
return restorable;
64+
}
65+
66+
function restoreAll(restorables: Restorable[]): void {
67+
for (const restorable of restorables.reverse()) {
68+
restorable.restore();
69+
}
70+
}
71+
72+
describe('TestContext', () => {
73+
let restorables: Restorable[];
74+
75+
beforeEach(() => {
76+
restorables = [];
77+
});
78+
79+
afterEach(() => {
80+
restoreAll(restorables);
81+
});
82+
83+
it('should log cleanup errors as warnings and rethrow them', async () => {
84+
const context = new TestContext({});
85+
const cleanupError = new Error('cleanup failed');
86+
const application = {
87+
isStopped: sinon.stub().returns(false),
88+
end: sinon.stub().rejects(cleanupError),
89+
};
90+
const warn = track(restorables, sinon.stub(loggerClient, 'warn'));
91+
92+
Object.defineProperty(context, 'application', {
93+
value: application,
94+
configurable: true,
95+
});
96+
97+
try {
98+
await context.end();
99+
chai.assert.fail('Expected context.end() to reject.');
100+
} catch (error) {
101+
chai.expect(error).to.equal(cleanupError);
102+
}
103+
104+
chai.expect(warn.calledOnceWithExactly(LOG_PREFIX, cleanupError)).to.be
105+
.equal(true);
106+
});
107+
});
108+
109+
describe('run', () => {
110+
let restorables: Restorable[];
111+
let events: RunEvents;
112+
let endStep: ReturnType<typeof sinon.stub>;
113+
114+
beforeEach(() => {
115+
restorables = [];
116+
prepareTestAPI();
117+
events = observeRunEvents();
118+
track(restorables, sinon.stub(loggerClient, 'startStep'));
119+
endStep = track(restorables, sinon.stub(loggerClient, 'endStep'));
120+
});
121+
122+
afterEach(() => {
123+
events.cleanup();
124+
restoreAll(restorables);
125+
});
126+
127+
it('should finish the test when body and cleanup pass', async () => {
128+
const end = track(restorables, sinon.stub(TestContext.prototype, 'end'));
129+
end.resolves();
130+
131+
await run(() => undefined);
132+
133+
chai.expect(end.calledOnce).to.equal(true);
134+
chai.expect(events.getFinishedCount()).to.equal(1);
135+
chai.expect(events.failedErrors).to.deep.equal([]);
136+
chai.expect(endStep.calledOnceWithExactly(TEST_ID, 'Test passed')).to
137+
.equal(true);
138+
});
139+
140+
it('should fail the test when cleanup fails after a passed body', async () => {
141+
const cleanupError = new Error('cleanup failed');
142+
const end = track(restorables, sinon.stub(TestContext.prototype, 'end'));
143+
end.rejects(cleanupError);
144+
145+
await run(() => undefined);
146+
147+
chai.expect(end.calledOnce).to.equal(true);
148+
chai.expect(events.getFinishedCount()).to.equal(0);
149+
chai.expect(events.failedErrors).to.deep.equal([cleanupError]);
150+
chai.expect(
151+
endStep.calledOnceWithExactly(
152+
TEST_ID,
153+
'Test failed',
154+
cleanupError,
155+
),
156+
).to.equal(true);
157+
});
158+
159+
it('should keep the body error primary when body and cleanup both fail', async () => {
160+
const bodyError = new Error('body failed');
161+
const cleanupError = new Error('cleanup failed');
162+
const end = track(restorables, sinon.stub(TestContext.prototype, 'end'));
163+
end.rejects(cleanupError);
164+
165+
await run(() => {
166+
throw bodyError;
167+
});
168+
169+
chai.expect(end.calledOnce).to.equal(true);
170+
chai.expect(events.getFinishedCount()).to.equal(0);
171+
chai.expect(events.failedErrors).to.deep.equal([bodyError]);
172+
chai.expect(
173+
endStep.calledOnceWithExactly(TEST_ID, 'Test failed', bodyError),
174+
).to.equal(true);
175+
});
176+
});

core/cli-config/src/default-config.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ export const defaultConfiguration: IConfig = {
1414
maxWriteThreadCount: 2,
1515
plugins: [],
1616
retryCount: 3,
17+
forceRetryCount: 0,
1718
retryDelay: 2000,
1819
testTimeout: 15 * 60 * 1000,
1920
logLevel: LogLevel.info,

core/cli-config/test/arguments-parser.spec.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ describe('argument parser', () => {
2222
const pluginsSet = ['plugin1', 'plugin2', 'plugin3'];
2323
const customFieldSet = '#P0,#P1,#P2';
2424
const customField = '#P0';
25+
const forceRetryCount = 3;
26+
const rcForceRetryCount = 4;
2527

2628
const argv = [
2729
'',
@@ -32,6 +34,7 @@ describe('argument parser', () => {
3234
`--plugins=${pluginsSet[0]}`,
3335
`--plugins=${pluginsSet[1]}`,
3436
`--plugins=${pluginsSet[2]}`,
37+
`--force-retry-count=${forceRetryCount}`,
3538
// value without assign
3639
'--tests',
3740
customTestsPath,
@@ -41,6 +44,8 @@ describe('argument parser', () => {
4144
customFieldSet,
4245
'--my-namespaced.second-custom-field',
4346
customField,
47+
'--rc.force-retry-count',
48+
`${rcForceRetryCount}`,
4449
];
4550

4651
const args = getArguments(argv);
@@ -49,6 +54,7 @@ describe('argument parser', () => {
4954
config: customConfigPath,
5055
tests: customTestsPath,
5156
plugins: pluginsSet,
57+
forceRetryCount,
5258
customField: customFieldSet,
5359
/* are the following needed ??? - looks like undocumented feature for early version
5460
// myNamespacedCustomField: customFieldSet,
@@ -58,6 +64,9 @@ describe('argument parser', () => {
5864
customField: customFieldSet,
5965
secondCustomField: customField,
6066
},
67+
rc: {
68+
forceRetryCount: rcForceRetryCount,
69+
},
6170
};
6271

6372
chai.expect(args).to.be.deep.equal(expected);

core/cli-config/test/get-config.spec.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,19 @@ describe('Get config', () => {
6363
chai.expect(config).to.have.property('workerLimit', override);
6464
});
6565

66+
it('should override force retry count with arguments', async () => {
67+
const forceRetryCount = 7;
68+
69+
const config = await getConfig([
70+
`--force-retry-count=${forceRetryCount}`,
71+
]);
72+
73+
chai.expect(config).to.have.property(
74+
'forceRetryCount',
75+
forceRetryCount,
76+
);
77+
});
78+
6679
it('should override every resolved config fields with arguments', async () => {
6780
const override = 'argumentsConfig';
6881

core/cli/src/index.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,12 @@ createField('retryCount', {
3535
type: 'number',
3636
});
3737

38+
createField('forceRetryCount', {
39+
describe:
40+
'Total forced attempts for every test; 0 disables force retry mode',
41+
type: 'number',
42+
});
43+
3844
createField('retryDelay', {
3945
describe: 'Time of delay before retry',
4046
type: 'number',

0 commit comments

Comments
 (0)