Skip to content

Commit eaefd59

Browse files
shubhamkdSamriddhi-35garg3133
authored
Add support for saving webdriver logs in parallel test run (#4440)
Co-authored-by: Samriddhi-35 <samriddhisharma632@gmail.com> Co-authored-by: Priyansh Garg <priyanshgarg30@gmail.com>
1 parent 646fc9c commit eaefd59

6 files changed

Lines changed: 247 additions & 3 deletions

File tree

lib/settings/defaults.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -228,6 +228,11 @@ module.exports = {
228228
// leave empty to use the test suite name when writing the webdriver server logs
229229
log_file_name: '',
230230

231+
// When running tests in parallel (test workers), each worker spawns its own webdriver process but
232+
// their logs are not written to disk by default. Set this to true to retain webdriver server logs
233+
// from worker processes when running in parallel mode.
234+
retain_logs_in_parallel_run: false,
235+
231236
// Time to wait (in ms) before starting to check the Webdriver server is up and running
232237
check_process_delay: 100,
233238

lib/transport/selenium-webdriver/index.js

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ const BaseTransport = require('../');
1111
const {colors} = Logger;
1212
const {isErrorResponse, checkLegacyResponse, throwDecodedError, WebDriverError} = error;
1313
const {IosSessionErrors} = require('../errors');
14+
const Concurrency = require('../../runner/concurrency/index.js');
1415

1516
let _driverService = null;
1617
let _driver = null;
@@ -195,6 +196,14 @@ class Transport extends BaseTransport {
195196
async createDriverService({options, moduleKey, reuseBrowser = false}) {
196197
try {
197198
moduleKey = this.settings.webdriver.log_file_name || moduleKey || '';
199+
if (Concurrency.isWorker()) {
200+
// append timestamp to the output file name to differentiate between the files
201+
// created across the workers.
202+
moduleKey = `${moduleKey}_${process.hrtime.bigint()}`;
203+
if (this.settings.webdriver.log_file_name) {
204+
this.settings.webdriver.log_file_name = moduleKey;
205+
}
206+
}
198207

199208
if (!this.shouldReuseDriverService(reuseBrowser)) {
200209
Transport.driverService = new this.ServiceBuilder(this.settings);
@@ -217,7 +226,7 @@ class Transport extends BaseTransport {
217226

218227

219228
async getDriver({options, reuseBrowser = false}) {
220-
const value = await this.shouldReuseDriver(reuseBrowser);
229+
const value = await this.shouldReuseDriver(reuseBrowser);
221230
if (value) {
222231
return Transport.driver;
223232
}

lib/transport/selenium-webdriver/service-builders/base-service.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -217,7 +217,9 @@ class BaseService {
217217
}
218218

219219
needsSinkProcess() {
220-
return !Concurrency.isWorker();
220+
const {retain_logs_in_parallel_run = false} = this.settings.webdriver || {};
221+
222+
return !Concurrency.isWorker() || retain_logs_in_parallel_run;
221223
}
222224

223225
hasSinkSupport() {
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
const assert = require('assert');
2+
const common = require('../../common.js');
3+
4+
describe('BaseService concurrency behaviour', function () {
5+
const originalParallelEnv = process.env.__NIGHTWATCH_PARALLEL_MODE;
6+
7+
after(function () {
8+
if (originalParallelEnv === undefined) {
9+
delete process.env.__NIGHTWATCH_PARALLEL_MODE;
10+
} else {
11+
process.env.__NIGHTWATCH_PARALLEL_MODE = originalParallelEnv;
12+
}
13+
});
14+
15+
function createService({isWorker, retainLogsInParallelRun} = {}) {
16+
// Concurrency.isWorker() reads this on each call (see lib/runner/concurrency/index.js)
17+
if (isWorker) {
18+
process.env.__NIGHTWATCH_PARALLEL_MODE = '1';
19+
} else {
20+
delete process.env.__NIGHTWATCH_PARALLEL_MODE;
21+
}
22+
23+
const BaseService = common.require('transport/selenium-webdriver/service-builders/base-service.js');
24+
25+
class MockBaseService extends BaseService {
26+
get requiresDriverBinary() {
27+
return false;
28+
}
29+
30+
async createSinkProcess() {
31+
this.sinkCreated = true;
32+
}
33+
34+
async createService(options) {
35+
// Mock the service object that BaseService.createService() expects
36+
this.service = {
37+
setPort: () => { },
38+
setHostname: () => { },
39+
setPath: () => { }
40+
};
41+
42+
return super.createService(options);
43+
}
44+
}
45+
46+
const settings = {
47+
webdriver: {
48+
log_path: 'logs',
49+
...(retainLogsInParallelRun ? {retain_logs_in_parallel_run: retainLogsInParallelRun} : {})
50+
}
51+
};
52+
53+
const service = new MockBaseService(settings);
54+
55+
return service;
56+
}
57+
58+
it('creates sink process when not running as worker', async function () {
59+
const service = createService({isWorker: false});
60+
61+
assert.strictEqual(service.needsSinkProcess(), true);
62+
63+
await service.createService({});
64+
if (process.platform === 'win32') {
65+
assert.strictEqual(service.sinkCreated, undefined);
66+
assert.strictEqual(service.settings.webdriver.log_path, false);
67+
} else {
68+
assert.strictEqual(service.sinkCreated, true);
69+
assert.strictEqual(service.settings.webdriver.log_path, 'logs');
70+
}
71+
});
72+
73+
it('does not create sink and disables log_path for workers by default', async function () {
74+
const service = createService({isWorker: true});
75+
76+
assert.strictEqual(service.needsSinkProcess(), false);
77+
78+
await service.createService({});
79+
80+
assert.strictEqual(service.sinkCreated, undefined);
81+
assert.strictEqual(service.settings.webdriver.log_path, false);
82+
});
83+
84+
it('creates sink and keeps log_path when retain_logs_in_parallel_run is true', async function () {
85+
const service = createService({isWorker: true, retainLogsInParallelRun: true});
86+
87+
assert.strictEqual(service.needsSinkProcess(), true);
88+
89+
await service.createService({});
90+
if (process.platform === 'win32') {
91+
assert.strictEqual(service.sinkCreated, undefined);
92+
assert.strictEqual(service.settings.webdriver.log_path, false);
93+
} else {
94+
assert.strictEqual(service.sinkCreated, true);
95+
assert.strictEqual(service.settings.webdriver.log_path, 'logs');
96+
}
97+
});
98+
});

test/src/service-builders/testSeleniumServer.js

Lines changed: 122 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -216,7 +216,8 @@ describe('SeleniumServer Transport Tests', function () {
216216
session,
217217
serverPath,
218218
serverPort,
219-
options
219+
options,
220+
client
220221
};
221222
}
222223

@@ -260,6 +261,126 @@ describe('SeleniumServer Transport Tests', function () {
260261
assert.ok(logFilePath.endsWith('testModuleKey_selenium-server.log'));
261262
});
262263

264+
it('test per-worker log file path when running in worker without log_file_name set', async function () {
265+
mockery.registerMock('../../runner/concurrency/index.js', {
266+
isWorker: function () {
267+
return true;
268+
}
269+
});
270+
271+
mockery.registerMock('geckodriver', {
272+
path: ''
273+
});
274+
275+
mockery.registerMock('chromedriver', {
276+
path: ''
277+
});
278+
279+
mockery.registerMock('@nightwatch/selenium-server', {
280+
path: '/path/to/selenium-server-standalone.3.0.jar'
281+
});
282+
283+
let logFilePath;
284+
const {client} = await SeleniumServerTestSetup({
285+
desiredCapabilities: {
286+
browserName: 'chrome'
287+
},
288+
selenium: {
289+
port: 9999,
290+
start_process: true
291+
}
292+
}, {
293+
onLogFile(filePath) {
294+
logFilePath = filePath;
295+
}
296+
});
297+
298+
// The log file name should include the moduleKey, timestamp and end with _selenium-server.log
299+
assert.ok(/testModuleKey_[0-9]+_selenium-server\.log$/.test(logFilePath));
300+
// The log_file_name should be unchanged
301+
assert.strictEqual(client.settings.webdriver.log_file_name, '');
302+
});
303+
304+
it('test log file path when not running in worker with log_file_name set', async function () {
305+
mockery.registerMock('geckodriver', {
306+
path: ''
307+
});
308+
309+
mockery.registerMock('chromedriver', {
310+
path: ''
311+
});
312+
313+
mockery.registerMock('@nightwatch/selenium-server', {
314+
path: '/path/to/selenium-server-standalone.3.0.jar'
315+
});
316+
317+
let logFilePath;
318+
const {client} = await SeleniumServerTestSetup({
319+
desiredCapabilities: {
320+
browserName: 'chrome'
321+
},
322+
selenium: {
323+
port: 9999,
324+
start_process: true
325+
},
326+
webdriver: {
327+
log_file_name: 'customModuleKey'
328+
}
329+
}, {
330+
onLogFile(filePath) {
331+
logFilePath = filePath;
332+
}
333+
});
334+
335+
// The log file should include the customModuleKey without the timestamp.
336+
assert.ok(logFilePath.endsWith('customModuleKey_selenium-server.log'));
337+
// No change in the log_file_name value as we're not running in worker
338+
assert.strictEqual(client.settings.webdriver.log_file_name, 'customModuleKey');
339+
});
340+
341+
it('test per-worker log file path when running in worker with log_file_name set', async function () {
342+
mockery.registerMock('../../runner/concurrency/index.js', {
343+
isWorker: function () {
344+
return true;
345+
}
346+
});
347+
348+
mockery.registerMock('geckodriver', {
349+
path: ''
350+
});
351+
352+
mockery.registerMock('chromedriver', {
353+
path: ''
354+
});
355+
356+
mockery.registerMock('@nightwatch/selenium-server', {
357+
path: '/path/to/selenium-server-standalone.3.0.jar'
358+
});
359+
360+
let logFilePath;
361+
const {client} = await SeleniumServerTestSetup({
362+
desiredCapabilities: {
363+
browserName: 'chrome'
364+
},
365+
selenium: {
366+
port: 9999,
367+
start_process: true
368+
},
369+
webdriver: {
370+
log_file_name: 'customModuleKey'
371+
}
372+
}, {
373+
onLogFile(filePath) {
374+
logFilePath = filePath;
375+
}
376+
});
377+
378+
// The log file name should include the customModuleKey, timestamp and end with _selenium-server.log
379+
assert.ok(/customModuleKey_[0-9]+_selenium-server\.log$/.test(logFilePath));
380+
// The log_file_name should also be updated to include the timestamp when running in worker
381+
assert.ok(/^customModuleKey_[0-9]+$/.test(client.settings.webdriver.log_file_name));
382+
});
383+
263384
it('test create session with selenium server 3 -- with drivers', async function() {
264385
mockery.registerMock('geckodriver', {
265386
path: '/path/to/geckodriver'

types/nightwatch-options.d.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -573,6 +573,15 @@ export interface WebdriverOptions {
573573
*/
574574
log_file_name?: string;
575575

576+
/**
577+
* When running tests in parallel (test workers), each worker spawns its own webdriver process but
578+
* their logs are not written to disk by default. Set this to true to retain webdriver server logs
579+
* from worker processes when running in parallel mode.
580+
*
581+
* @default false
582+
*/
583+
retain_logs_in_parallel_run?: boolean;
584+
576585
/**
577586
* List of cli arguments to be passed to the Webdriver process. This varies for each Webdriver implementation.
578587
*

0 commit comments

Comments
 (0)