Skip to content

Commit d146218

Browse files
authored
feat: Wait for tunnel to be ready before starting tests (#21)
* Add tunnel api * feat: check for tunnel readiness * Encapsulate error * Api error handling * User facing time in seconds not milliseconds * Consistent error messages * Better naming * Sleep a bit longer if tunnel api call errored * Don't hammer the api if auth is failing * copy editing * default to 30s
1 parent e7f2562 commit d146218

File tree

5 files changed

+144
-1
lines changed

5 files changed

+144
-1
lines changed

README.md

+2
Original file line numberDiff line numberDiff line change
@@ -81,3 +81,5 @@ Optional environment variables:
8181
- `SAUCE_TAGS` - A comma separated list of tags to apply to all jobs.
8282
- `SAUCE_REGION` - The Sauce Labs region. Valid values are `us-west-1` (default) or `eu-central-1`.
8383
- `SAUCE_SCREEN_RESOLUTION` - The desktop browser screen resolution (not applicable to mobile). The format is `1920x1080`.
84+
- `SAUCE_TUNNEL_WAIT_SEC` - The amount of time to wait, in seconds, for
85+
the tunnel defined by `SAUCE_TUNNEL_NAME` to be ready. Default is "30".

src/api.ts

+61-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import axios from 'axios';
1+
import axios, { isAxiosError } from 'axios';
22

33
export interface Platform {
44
/**
@@ -29,6 +29,18 @@ export interface Platform {
2929
os: string;
3030
}
3131

32+
export interface Tunnel {
33+
id: string;
34+
owner: string;
35+
status: string;
36+
tunnel_identifier: string;
37+
}
38+
39+
export type ApiResult<T, E> =
40+
| { kind: 'ok'; data: T }
41+
| { kind: 'err'; error: E }
42+
| { kind: 'unauthorized' };
43+
3244
export async function getPlatforms(params: {
3345
username: string;
3446
accessKey: string;
@@ -46,3 +58,51 @@ export async function getPlatforms(params: {
4658

4759
return resp;
4860
}
61+
62+
export async function getTunnels(params: {
63+
username: string;
64+
accessKey: string;
65+
region: string;
66+
filter: string;
67+
}): Promise<ApiResult<{ [key: string]: Tunnel[] }, Error>> {
68+
const { username, accessKey, region, filter } = params;
69+
try {
70+
const resp = await axios.get<{ [key: string]: Tunnel[] }>(
71+
`https://api.${region}.saucelabs.com/rest/v1/${username}/tunnels`,
72+
{
73+
auth: {
74+
username,
75+
password: accessKey,
76+
},
77+
params: {
78+
full: true,
79+
all: true,
80+
filter: filter !== '' ? filter : undefined,
81+
},
82+
},
83+
);
84+
85+
return {
86+
kind: 'ok',
87+
data: resp.data,
88+
};
89+
} catch (e) {
90+
if (isAxiosError(e)) {
91+
if (e.response?.status === 401) {
92+
return {
93+
kind: 'unauthorized',
94+
};
95+
}
96+
return {
97+
kind: 'err',
98+
error: new Error(
99+
`unexpected response (${e.status}) fetching tunnels: ${e.message}`,
100+
),
101+
};
102+
}
103+
return {
104+
kind: 'err',
105+
error: new Error(`unknown error fetching tunnels: ${e}`),
106+
};
107+
}
108+
}

src/errors.ts

+7
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,13 @@ export class TunnelNameError extends Error {
1818
}
1919
}
2020

21+
export class TunnelNotReadyError extends Error {
22+
constructor() {
23+
super('Timed out waiting for a tunnel to be ready.');
24+
this.name = 'TunnelNotReadyError';
25+
}
26+
}
27+
2128
export class CreateSessionError extends Error {
2229
constructor() {
2330
super('Failed to run test on Sauce Labs: no session id returned');

src/index.ts

+20
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,13 @@ import {
33
AuthError,
44
InvalidRegionError,
55
TunnelNameError,
6+
TunnelNotReadyError,
67
WindowSizeRangeError,
78
} from './errors';
89
import { getPlatforms } from './api';
910
import { rcompareOses, rcompareVersions } from './sort';
1011
import { isDevice } from './device';
12+
import { waitForTunnel } from './tunnel';
1113

1214
type Browser = string;
1315
type Version = string;
@@ -45,6 +47,7 @@ module.exports = {
4547
const username = process.env.SAUCE_USERNAME;
4648
const accessKey = process.env.SAUCE_ACCESS_KEY;
4749
const tunnelName = process.env.SAUCE_TUNNEL_NAME;
50+
const tunnelWait = Number(process.env.SAUCE_TUNNEL_WAIT_SEC) || 30;
4851
const build = process.env.SAUCE_BUILD;
4952
const tags = (process.env.SAUCE_TAGS || '').split(',');
5053
const region = process.env.SAUCE_REGION || 'us-west-1';
@@ -64,6 +67,23 @@ module.exports = {
6467
throw new InvalidRegionError();
6568
}
6669

70+
console.log(
71+
`Waiting up to ${tunnelWait}s for tunnel "${tunnelName}" to be ready...`,
72+
);
73+
const tunnelStatus = await waitForTunnel(
74+
username,
75+
accessKey,
76+
region,
77+
tunnelName,
78+
tunnelWait,
79+
);
80+
if (tunnelStatus === 'notready') {
81+
throw new TunnelNotReadyError();
82+
} else if (tunnelStatus === 'unauthorized') {
83+
throw new AuthError();
84+
}
85+
console.log('Tunnel is ready');
86+
6787
sauceDriver = new SauceDriver(
6888
username,
6989
accessKey,

src/tunnel.ts

+54
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import { getTunnels } from './api';
2+
3+
async function sleep(delay: number) {
4+
return new Promise((resolve) => setTimeout(resolve, delay));
5+
}
6+
7+
export async function waitForTunnel(
8+
username: string,
9+
accessKey: string,
10+
region: string,
11+
tunnelName: string,
12+
wait: number,
13+
): Promise<'ok' | 'notready' | 'unauthorized'> {
14+
return await Promise.race([
15+
(async function (): Promise<'ok' | 'unauthorized'> {
16+
// eslint-disable-next-line no-constant-condition
17+
while (true) {
18+
const result = await getTunnels({
19+
username,
20+
accessKey,
21+
region,
22+
filter: tunnelName,
23+
});
24+
if (result.kind === 'unauthorized') {
25+
return 'unauthorized';
26+
}
27+
if (result.kind !== 'ok') {
28+
await sleep(2000);
29+
continue;
30+
}
31+
32+
const allTunnels = result.data;
33+
for (const owner in allTunnels) {
34+
const tunnels = allTunnels[owner];
35+
if (
36+
tunnels.some(
37+
(t) =>
38+
t.owner === username &&
39+
(t.tunnel_identifier === tunnelName || t.id === tunnelName) &&
40+
t.status === 'running',
41+
)
42+
) {
43+
return 'ok';
44+
}
45+
}
46+
await sleep(1000);
47+
}
48+
})(),
49+
(async function (): Promise<'notready'> {
50+
await sleep(wait * 1000);
51+
return 'notready';
52+
})(),
53+
]);
54+
}

0 commit comments

Comments
 (0)