Skip to content

Commit

Permalink
Merge pull request #208 from christian-bromann/cb-sauce-update-fix
Browse files Browse the repository at this point in the history
fix: start sauce connect via saucelabs package
  • Loading branch information
wswebcreation committed May 25, 2020
2 parents 9e0a3db + 81b95d9 commit 3da30a7
Show file tree
Hide file tree
Showing 7 changed files with 919 additions and 360 deletions.
1 change: 0 additions & 1 deletion examples/karma.conf-ci.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,6 @@ module.exports = function (config) {
testName: 'Karma and Sauce Labs demo',
recordScreenshots: false,
connectOptions: {
port: 5757,
logfile: 'sauce_connect.log'
},
public: 'public'
Expand Down
3 changes: 1 addition & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,7 @@
"author": "Vojta Jina <vojta.jina@gmail.com>",
"dependencies": {
"global-agent": "^2.1.8",
"sauce-connect-launcher-update": "^1.3.2",
"saucelabs": "^4.0.2",
"saucelabs": "^4.3.0",
"webdriverio": "^6.1.9"
},
"license": "MIT",
Expand Down
24 changes: 13 additions & 11 deletions src/launcher/launcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@ import {remote, BrowserObject} from 'webdriverio';
import {processConfig} from "../process-config";
import {BrowserMap} from "../browser-info";

// Array of connected drivers. This is useful for quitting all connected drivers on kill.
let connectedDrivers: Map<string, BrowserObject> = new Map();

export function SaucelabsLauncher(args,
/* config.sauceLabs */ config,
/* SauceConnect */ sauceConnect,
Expand All @@ -16,6 +19,9 @@ export function SaucelabsLauncher(args,
captureTimeoutLauncherDecorator(this);
retryLauncherDecorator(this);

// initiate driver with null to not close the tunnel too early
connectedDrivers.set(this.id, null)

const log = logger.create('SaucelabsLauncher');
const {
startConnect,
Expand All @@ -24,9 +30,6 @@ export function SaucelabsLauncher(args,
browserName
} = processConfig(config, args);

// Array of connected drivers. This is useful for quitting all connected drivers on kill.
let connectedDrivers: BrowserObject[] = [];

// Setup Browser name that will be printed out by Karma.
this.name = browserName + ' on SauceLabs';

Expand All @@ -39,7 +42,7 @@ export function SaucelabsLauncher(args,
// In case the "startConnect" option has been enabled, establish a tunnel and wait
// for it being ready. In case a tunnel is already active, this will just continue
// without establishing a new one.
await sauceConnect.establishTunnel(sauceConnectOptions);
await sauceConnect.establishTunnel(seleniumCapabilities, sauceConnectOptions);
} catch (error) {
log.error(error);

Expand All @@ -55,7 +58,7 @@ export function SaucelabsLauncher(args,

// Keep track of all connected drivers because it's possible that there are multiple
// driver instances (e.g. when running with concurrency)
connectedDrivers.push(driver);
connectedDrivers.set(this.id, driver);

const sessionId = driver.sessionId

Expand All @@ -81,9 +84,10 @@ export function SaucelabsLauncher(args,
}
});

this.on('kill', async (doneFn: () => void) => {
this.on('kill', async (done: () => void) => {
try {
await Promise.all(connectedDrivers.map(driver => driver.deleteSession()));
const driver = connectedDrivers.get(this.id);
await driver.deleteSession();
} catch (e) {
// We need to ignore the exception here because we want to make sure that Karma is still
// able to retry connecting if Saucelabs itself terminated the session (and not Karma)
Expand All @@ -93,9 +97,7 @@ export function SaucelabsLauncher(args,
log.error(e);
}

// Reset connected drivers in case the launcher will be reused.
connectedDrivers = [];

doneFn();
connectedDrivers.delete(this.id)
return process.nextTick(done);
})
}
38 changes: 20 additions & 18 deletions src/local-tunnel/sauceconnect.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,4 @@
import {promisify} from 'util';

// This import lacks type definitions.
const launchSauceConnect = promisify(require('sauce-connect-launcher-update'));
import SaucelabsAPI, {SauceConnectInstance} from 'saucelabs';

/**
* Service that can be used to create a SauceConnect tunnel automatically. This can be used
Expand All @@ -10,14 +7,11 @@ const launchSauceConnect = promisify(require('sauce-connect-launcher-update'));
export function SauceConnect(emitter, logger) {
const log = logger.create('launcher.SauceConnect');

// Currently active tunnel instance. See: https://github.com/bermi/sauce-connect-launcher
// Currently active tunnel instance. See: https://github.com/saucelabs/node-saucelabs
// for public API.
let activeInstancePromise: Promise<any> = null;

this.establishTunnel = async (connectOptions: any) => {
// Redirect all logging output to Karma's logger.
connectOptions.logger = log.debug.bind(log);

this.establishTunnel = async (seleniumCapabilities, sauceConnectOptions: any) => {
// In case there is already a promise for a SauceConnect tunnel, we still need to return the
// promise because we want to make sure that the launcher can wait in case the tunnel is
// still starting.
Expand All @@ -26,21 +20,29 @@ export function SauceConnect(emitter, logger) {
}

// Open a new SauceConnect tunnel.
return activeInstancePromise = launchSauceConnect(connectOptions);
const api = new SaucelabsAPI(seleniumCapabilities)
return activeInstancePromise = api.startSauceConnect({
// Redirect all logging output to Karma's logger.
logger: log.debug.bind(log),
...sauceConnectOptions
});
};

// Close the tunnel whenever Karma emits the "exit" event. In that case, we don't need to
// reset the state because Karma will exit completely.
emitter.on('exit', (doneFn: () => void) => {
emitter.on('exit', async (doneFn: () => void) => {
if (activeInstancePromise) {
log.debug('Shutting down Sauce Connect');

// Close the tunnel and notify Karma once the tunnel has been exited.
activeInstancePromise
.then(instance => instance.close(doneFn))
.catch(() => doneFn())
} else {
doneFn();
// shut down Sauce Connect once all session have been terminated
try {
const tunnelInstance:SauceConnectInstance = await activeInstancePromise
await tunnelInstance.close()
} catch (err) {
log.error(`Could not close Sauce Connect Tunnel. Failure message: ${err.stack}`);
}
}

doneFn();
})
}
}
5 changes: 1 addition & 4 deletions src/process-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,10 +48,6 @@ export function processConfig (config: any = {}, args: any = {}) {
};

const sauceConnectOptions = {
// By default, we just pass in the general Saucelabs credentials for establishing the
// SauceConnect tunnel. This makes it possible to use "startConnect" with no additional setup.
username: username,
accessKey: accessKey,
tunnelIdentifier: tunnelIdentifier,
...config.connectOptions,
};
Expand All @@ -68,6 +64,7 @@ export function processConfig (config: any = {}, args: any = {}) {
key: accessKey,
region: config.region,
headless: config.headless,
logLevel: 'error',
capabilities: {
'sauce:options': capabilitiesFromConfig,
...args
Expand Down
21 changes: 11 additions & 10 deletions src/reporter/reporter.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import {promisify} from 'util';
import {BrowserMap} from "../browser-info";
import SaucelabsAPI, {Job} from 'saucelabs';

Expand Down Expand Up @@ -33,28 +32,30 @@ export function SaucelabsReporter(logger, browserMap: BrowserMap) {
}

const {sessionId} = browserData;
const apiInstance = new SaucelabsAPI({
const api = new SaucelabsAPI({
user: browserData.username,
key: browserData.accessKey,
region: browserData.region,
headless: browserData.headless
});
const updateJob = promisify(apiInstance.updateJob.bind(apiInstance));
const hasPassed = !(result.failed || result.error || result.disconnected);
const hasPassed = !result.failed && !result.error && !result.disconnected;

// Update the job by reporting the test results. Also we need to store the promise here
// because in case "onExit" is being called, we want to wait for the API calls to finish.
pendingUpdates.push(updateJob(browserData.username, sessionId, {
pendingUpdates.push(api.updateJob(browserData.username, sessionId, {
id: sessionId,
passed: hasPassed,
'custom-data': result
}).catch(error => log.error('Could not report results to Saucelabs: %s', error)));
}));
};

// Whenever this method is being called, we just need to wait for all API calls to finish,
// and then we can notify Karma about proceeding with the exit.
this.onExit = async (doneFn: () => void) => {
await Promise.all(pendingUpdates);
doneFn();
}
this.onExit = (doneFn: () => void) => Promise.all(pendingUpdates).then(
doneFn,
(error) => {
log.error('Could not report results to Saucelabs: %s', error)
doneFn()
}
);
}
Loading

0 comments on commit 3da30a7

Please sign in to comment.