Skip to content

Commit 39b6920

Browse files
improve api handling
1 parent 6f539f2 commit 39b6920

File tree

5 files changed

+143
-44
lines changed

5 files changed

+143
-44
lines changed

package-lock.json

Lines changed: 2 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/commands/run.ts

Lines changed: 44 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import log from '../log';
22
import Archiver from '../utils/archiver';
33
import Uploader from '../utils/uploader';
44
import Poller from '../utils/poller';
5-
import Config from '../utils/config';
5+
import Config, { IConfig } from '../utils/config';
66
import Tunnel from '../utils/tunnel';
77
import ora from 'ora';
88
import chalk from 'chalk';
@@ -19,15 +19,46 @@ export default class RunProject {
1919
private poller: Poller | undefined = undefined;
2020
private tunnel: Tunnel | undefined = undefined;
2121
private configFilePath: string | undefined = undefined;
22+
private config: IConfig | undefined = undefined;
2223

2324
constructor(argv: Arguments) {
2425
if (typeof(argv.cf) === 'string') {
2526
this.configFilePath = argv.cf;
2627
}
2728
}
2829

30+
public exitHandler(): void {
31+
if (this.config && this.config.run_settings.start_tunnel) {
32+
if (this.tunnel) {
33+
this.tunnel.stop().catch(console.error);
34+
}
35+
}
36+
}
37+
38+
public errorHandler(): void {
39+
console.error(chalk.white.bgRed.bold(`A fatal error occurred, please report this to testingbot.com`));
40+
this.exitHandler();
41+
}
42+
43+
private registerCloseHandlers(): void {
44+
//do something when app is closing
45+
process.on('exit', this.exitHandler.bind(this,{cleanup:true}));
46+
process.on('SIGINT', this.exitHandler.bind(this, {exit:true}));
47+
process.on('SIGUSR1', this.exitHandler.bind(this, {exit:true}));
48+
process.on('SIGUSR2', this.exitHandler.bind(this, {exit:true}));
49+
process.on('uncaughtException', this.errorHandler.bind(this, {exit:true}));
50+
}
51+
52+
private realTimeMessage(message: string): void {
53+
console.log(message);
54+
}
55+
56+
private realTimeError(message: string): void {
57+
console.error(message);
58+
}
59+
2960
public async start(): Promise<void> {
30-
let config;
61+
let config: IConfig;
3162
try {
3263
config = await Config.getConfig(this.configFilePath || `testingbot.json`);
3364
} catch (e) {
@@ -42,6 +73,8 @@ export default class RunProject {
4273
return;
4374
}
4475

76+
this.config = config;
77+
4578
this.archiver = new Archiver(config);
4679
this.uploader = new Uploader(config);
4780
this.poller = new Poller(config);
@@ -68,37 +101,27 @@ export default class RunProject {
68101
return;
69102
}
70103

104+
this.registerCloseHandlers();
105+
71106
try {
72107
const response = await this.uploader.start(zipFile);
73108
uploadSpinner.succeed('Cypress is now running on TestingBot')
74-
console.log('will join')
75109
const realTime = io.connect('https://hub.testingbot.com:3031');
76-
console.log('joining', `cypress_${response.id}`)
77110
realTime.emit('join', `cypress_${response.id}`)
78-
realTime.on('connect', () => {
79-
console.log('connected')
80-
})
81-
realTime.on('disconnect', () => {
82-
console.log('disconnect')
83-
})
84-
realTime.on('event', (data: any) => {
85-
console.log('event', data)
86-
});
87-
realTime.on('error', (err: any) => {
88-
console.error(err)
89-
})
90-
realTime.on("cypress_data", (msg: any) => console.log(msg));
91-
realTime.on("cypress_error", (msg: any) => console.log(msg));
92-
93-
const poller = await this.poller.check(response.id, uploadSpinner)
111+
realTime.on('cypress_data', (msg: string) => this.realTimeMessage.bind(this, msg));
112+
realTime.on('cypress_error', (msg: string) => this.realTimeError.bind(this, msg));
113+
114+
const poller = await this.poller.check(response.id, uploadSpinner);
94115
log.info(poller)
95116

117+
process.exit(0);
118+
96119
} catch (err) {
97120
console.error(chalk.white.bgRed.bold(err));
98121
if (config.run_settings.start_tunnel) {
99122
await this.tunnel.stop();
100123
}
101-
return;
124+
process.exit(1);
102125
}
103126
}
104127
}

src/utils/archiver.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,21 @@ export default class Archiver {
7070
});
7171
});
7272

73+
let packageJSON = {};
74+
75+
if (typeof this.config.run_settings.package_config_options === 'object') {
76+
Object.assign(packageJSON, this.config.run_settings.package_config_options);
77+
}
78+
79+
if (typeof this.config.run_settings.npm_dependencies === 'object') {
80+
Object.assign(packageJSON, {devDependencies: this.config.run_settings.npm_dependencies});
81+
}
82+
83+
if (Object.keys(packageJSON).length > 0) {
84+
let packageJSONString = JSON.stringify(packageJSON, null, 4);
85+
archive.append(packageJSONString, { name: 'testingbot-package.json' });
86+
}
87+
7388
archive.finalize();
7489
} catch (e) {
7590
reject(e);

src/utils/config.ts

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,9 +34,20 @@ export interface IConfig {
3434
export default {
3535
async getConfig(configFilePath: string): Promise<IConfig> {
3636
const configString = await fsPromises.readFile(configFilePath);
37-
const configObject = JSON.parse(configString.toString());
37+
const configObject: IConfig = JSON.parse(configString.toString());
3838

39-
return configObject as IConfig;
39+
if (configObject.auth.key === '' || configObject.auth.key === '<Your TestingBot key>') {
40+
if (process.env.TESTINGBOT_KEY) {
41+
configObject.auth.key = process.env.TESTINGBOT_KEY;
42+
}
43+
}
44+
if (configObject.auth.secret === '' || configObject.auth.secret === '<Your TestingBot secret>') {
45+
if (process.env.TESTINGBOT_SECRET) {
46+
configObject.auth.secret = process.env.TESTINGBOT_SECRET;
47+
}
48+
}
49+
50+
return configObject;
4051
},
4152

4253
validate(config: IConfig): string[] {

src/utils/poller.ts

Lines changed: 69 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,28 @@ import log from './../log';
33
import { IConfig } from './config';
44
import ora from 'ora';
55

6-
interface IPollResponse {
7-
status: string
6+
interface ITest {
7+
sessionId: string
8+
}
9+
10+
enum IStatus {
11+
'WAITING',
12+
'READY',
13+
'FAILED',
14+
'DONE'
15+
}
16+
17+
interface IRun {
18+
status: IStatus
19+
capabilities: any
820
errors: string[]
21+
test?: ITest
22+
}
23+
24+
interface IPollResponse {
25+
runs: IRun[]
26+
version: string
27+
build?: string
928
}
1029

1130
export default class Poller {
@@ -22,12 +41,15 @@ export default class Poller {
2241
public async check(id: number, spinner: ora.Ora): Promise<IPollResponse> {
2342
return new Promise((resolve, reject) => {
2443
this.intervalId = setInterval(async () => {
25-
const response = await this.getStatus(id);
26-
27-
if (response.status === 'DONE') {
44+
const response = await this.getApiResponse(id);
45+
const status = this.getStatus(response);
46+
47+
if (status === IStatus.DONE) {
2848
spinner.succeed('Cypress Project has finished running on TestingBot');
2949
log.info(response);
30-
if (Object.keys(response.errors).length === 0) {
50+
51+
const errors = this.getErrors(response);
52+
if (errors.length === 0) {
3153
if (this.intervalId) {
3254
clearInterval(this.intervalId);
3355
this.intervalId = undefined;
@@ -39,16 +61,16 @@ export default class Poller {
3961
clearInterval(this.intervalId);
4062
this.intervalId = undefined;
4163
}
42-
return reject(response);
64+
return reject(errors);
4365
}
4466

45-
if (response.status === 'WAITING' && this.retryNumber > Poller.MAX_RETRIES_WAITING) {
67+
if (status === IStatus.WAITING && this.retryNumber > Poller.MAX_RETRIES_WAITING) {
4668
if (this.intervalId) {
4769
clearInterval(this.intervalId);
4870
this.intervalId = undefined;
4971
}
5072
return reject(new Error(`Waited too long to retrieve information, please try again later.`));
51-
} else if (response.status === 'READY' && this.retryNumber > Poller.MAX_RETRIES_READY) {
73+
} else if (status === IStatus.READY && this.retryNumber > Poller.MAX_RETRIES_READY) {
5274
if (this.intervalId) {
5375
clearInterval(this.intervalId);
5476
this.intervalId = undefined;
@@ -61,7 +83,36 @@ export default class Poller {
6183
});
6284
}
6385

64-
private async getStatus(id: number): Promise<IPollResponse> {
86+
private getErrors(response: IPollResponse): string[] {
87+
const errors: string[] = []
88+
for (let i = 0; i < response.runs.length; i++) {
89+
const testRun = response.runs[i];
90+
if (testRun.errors.length > 0) {
91+
errors.concat(testRun.errors);
92+
}
93+
}
94+
95+
return errors;
96+
}
97+
98+
private getStatus(response: IPollResponse): IStatus {
99+
let status = IStatus.DONE
100+
101+
for (let i = 0; i < response.runs.length; i++) {
102+
const testRun = response.runs[i];
103+
if (testRun.status === IStatus.DONE) {
104+
continue;
105+
} else if (testRun.status === IStatus.WAITING) {
106+
// whenever one is waiting, the whole batch is waiting
107+
return IStatus.WAITING;
108+
}
109+
status = testRun.status;
110+
}
111+
112+
return status;
113+
}
114+
115+
private async getApiResponse(id: number): Promise<IPollResponse> {
65116
return new Promise((resolve, reject) => {
66117
const requestOptions = {
67118
method: 'GET',
@@ -77,16 +128,14 @@ export default class Poller {
77128
if (error) {
78129
return reject(error);
79130
}
80-
let responseBody: IPollResponse = { status: 'WAITING', errors: [] };
81-
if (response) {
82-
if (response.body && typeof response.body === 'string') {
83-
response.body = JSON.parse(response.body) as IPollResponse;
84-
}
85-
if (response.statusCode.toString().substring(0, 1) === '2') {
86-
responseBody = response.body;
87-
} else {
88-
return reject(response.body);
89-
}
131+
let responseBody: IPollResponse;
132+
if (response.body && typeof response.body === 'string') {
133+
response.body = JSON.parse(response.body) as IPollResponse;
134+
}
135+
if (response.statusCode.toString().substring(0, 1) === '2') {
136+
responseBody = response.body;
137+
} else {
138+
return reject(response.body);
90139
}
91140

92141
resolve(responseBody);

0 commit comments

Comments
 (0)