Skip to content

Commit

Permalink
feat: add default global timeout (#237)
Browse files Browse the repository at this point in the history
* refactor: remove unused fields

* refactor: remove unused fields

* refactor: camel case

* feat: add default global timeout

* refactor: simplify result type handling

* refactor: remove unused type

* feat: remove playwright's native global timeout

* fix: remove incorrect test timeout
  • Loading branch information
Alex Plischke authored Oct 16, 2023
1 parent 1d2db3c commit bde42ab
Show file tree
Hide file tree
Showing 6 changed files with 46 additions and 97 deletions.
47 changes: 19 additions & 28 deletions src/cucumber-runner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import {spawn} from 'node:child_process';
import * as path from 'node:path';
import {prepareNpmEnv, preExec} from 'sauce-testrunner-utils';

import type {CucumberRunnerConfig, Metrics, RunResult} from './types';
import type {CucumberRunnerConfig} from './types';
import * as utils from './utils';

function buildArgs(runCfg: CucumberRunnerConfig, cucumberBin: string) {
Expand Down Expand Up @@ -62,7 +62,7 @@ function buildArgs(runCfg: CucumberRunnerConfig, cucumberBin: string) {
return procArgs;
}

export async function runCucumber(nodeBin: string, runCfg: CucumberRunnerConfig): Promise<RunResult> {
export async function runCucumber(nodeBin: string, runCfg: CucumberRunnerConfig): Promise<boolean> {
process.env.BROWSER_NAME = runCfg.suite.browserName;
process.env.BROWSER_OPTIONS = runCfg.suite.browserOptions;
process.env.SAUCE_SUITE_NAME = runCfg.suite.name;
Expand All @@ -78,49 +78,40 @@ export async function runCucumber(nodeBin: string, runCfg: CucumberRunnerConfig)
const nodeCtx = {nodePath: nodeBin, npmPath: npmBin};

// Install NPM dependencies
const metrics: Metrics[] = [];
const npmMetrics = await prepareNpmEnv(runCfg, nodeCtx);
metrics.push(npmMetrics);
await prepareNpmEnv(runCfg, nodeCtx);

const startTime = new Date().toISOString();
// Run suite preExecs
if (!await preExec.run(runCfg.suite, runCfg.preExecTimeout)) {
return {
startTime,
endTime: new Date().toISOString(),
hasPassed: false,
metrics,
};
return false;
}

const cucumberBin = path.join(runCfg.projectPath, 'node_modules', '@cucumber', 'cucumber', 'bin', 'cucumber-js');
const procArgs = buildArgs(runCfg, cucumberBin);
const proc = spawn(nodeBin, procArgs, {stdio: 'inherit', env: process.env});

let passed = false;
const procPromise = new Promise<number | null>((resolve, reject) => {
proc.on('error', (err) => {
reject(err);
});
// saucectl suite.timeout is in nanoseconds, convert to seconds
const timeout = (runCfg.suite.timeout || 0) / 1_000_000_000 || 30 * 60; // 30min default

const timeoutPromise = new Promise<boolean>((resolve) => {
setTimeout(() => {
console.error(`Job timed out after ${timeout} seconds`);
resolve(false);
}, timeout * 1000);
});

const cucumberPromise = new Promise<boolean>((resolve) => {
proc.on('close', (code) => {
resolve(code);
resolve(code === 0);
});
});

try {
const exitCode = await procPromise;
passed = exitCode === 0;
return await Promise.race([timeoutPromise, cucumberPromise]);
} catch (e) {
console.error(`Could not complete job. Reason: ${e}`);
console.error(`Failed to run Cucumber.js: ${e}`);
}
const endTime = new Date().toISOString();

return {
startTime,
endTime,
hasPassed: passed,
metrics,
};
return false;
}

function buildFormatOption(cfg: CucumberRunnerConfig) {
Expand Down
61 changes: 24 additions & 37 deletions src/playwright-runner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,7 @@ import * as convert from 'xml-js';
import {runCucumber} from './cucumber-runner';
import type {
CucumberRunnerConfig,
Metrics,
RunnerConfig,
RunResult,
} from './types';
import * as utils from './utils';

Expand All @@ -24,7 +22,7 @@ function getPlatformName(platformName: string) {
return platformName;
}

function generateJunitfile(sourceFile: string, suiteName: string, browserName: string, platformName: string) {
function generateJunitFile(sourceFile: string, suiteName: string, browserName: string, platformName: string) {
if (!fs.existsSync(sourceFile)) {
return;
}
Expand Down Expand Up @@ -168,22 +166,22 @@ async function run(nodeBin: string, runCfgPath: string, suiteName: string) {
console.log(`Sauce Playwright Runner ${packageInfo.version}`);
console.log(`Running Playwright ${packageInfo.dependencies?.playwright || ''}`);

let result: RunResult;
let passed = false;
if (runCfg.Kind === 'playwright-cucumberjs') {
result = await runCucumber(nodeBin, runCfg);
passed = await runCucumber(nodeBin, runCfg);
} else {
result = await runPlaywright(nodeBin, runCfg);
passed = await runPlaywright(nodeBin, runCfg);
try {
generateJunitfile(runCfg.junitFile, runCfg.suite.name, runCfg.suite.param.browser, runCfg.suite.platformName);
generateJunitFile(runCfg.junitFile, runCfg.suite.name, runCfg.suite.param.browser, runCfg.suite.platformName);
} catch (err) {
console.error(`Failed to generate junit file: ${err}`);
}
}

return result.hasPassed;
return passed;
}

async function runPlaywright(nodeBin: string, runCfg: RunnerConfig): Promise<RunResult> {
async function runPlaywright(nodeBin: string, runCfg: RunnerConfig): Promise<boolean> {
const excludeParams = ['screenshot-on-failure', 'video', 'slow-mo', 'headless', 'headed'];

process.env.BROWSER_NAME = runCfg.suite.param.browserName;
Expand Down Expand Up @@ -272,54 +270,43 @@ async function runPlaywright(nodeBin: string, runCfg: RunnerConfig): Promise<Run

utils.setEnvironmentVariables(env);

// Install NPM dependencies
const metrics: Metrics[] = [];

// Define node/npm path for execution
const npmBin = path.join(path.dirname(nodeBin), 'node_modules', 'npm', 'bin', 'npm-cli.js');
const nodeCtx = { nodePath: nodeBin, npmPath: npmBin };

// runCfg.path must be set for prepareNpmEnv to find node_modules. :(
const npmMetrics = await prepareNpmEnv(runCfg, nodeCtx);
metrics.push(npmMetrics);
await prepareNpmEnv(runCfg, nodeCtx);

const startTime = new Date().toISOString();
// Run suite preExecs
if (!await preExec.run(suite, runCfg.preExecTimeout)) {
return {
startTime,
endTime: new Date().toISOString(),
hasPassed: false,
metrics,
};
return false;
}

const playwrightProc = spawn(nodeBin, procArgs, {stdio: 'inherit', cwd: runCfg.projectPath, env});

const playwrightPromise = new Promise<number | null>((resolve, reject) => {
playwrightProc.on('error', (err) => {
reject(err);
});
// saucectl suite.timeout is in nanoseconds, convert to seconds
const timeout = (runCfg.suite.timeout || 0) / 1_000_000_000 || 30 * 60; // 30min default

const timeoutPromise = new Promise<boolean>((resolve) => {
setTimeout(() => {
console.error(`Job timed out after ${timeout} seconds`);
resolve(false);
}, timeout * 1000);
});

const playwrightPromise = new Promise<boolean>((resolve) => {
playwrightProc.on('close', (code) => {
resolve(code);
resolve(code === 0);
});
});

let hasPassed = false;
try {
const exitCode = await playwrightPromise;
hasPassed = exitCode === 0;
return await Promise.race([timeoutPromise, playwrightPromise]);
} catch (e) {
console.error(`Could not complete job. Reason: ${e}`);
console.error(`Failed to run Playwright: ${e}`);
}
const endTime = new Date().toISOString()

return {
startTime,
endTime,
hasPassed,
metrics,
};
return false;
}

if (require.main === module) {
Expand Down
6 changes: 0 additions & 6 deletions src/sauce.config.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -26,12 +26,6 @@ for (const file of configFiles) {
}
}

// Set a default timeout of 30 minutes if one is not provided. This protects
// against tests that hang and never finish.
if (!userConfig.globalTimeout) {
userConfig.globalTimeout = 1000 * 60 * 30; // 30 minutes
}

const overrides = {
use: {
headless: process.env.HEADLESS === 'true',
Expand Down
6 changes: 0 additions & 6 deletions src/sauce.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,6 @@ for (const file of configFiles) {
}
}

// Set a default timeout of 30 minutes if one is not provided. This protects
// against tests that hang and never finish.
if (!userConfig.globalTimeout) {
userConfig.globalTimeout = 1000 * 60 * 30; // 30 minutes
}

const overrides = {
use: {
headless: process.env.HEADLESS === 'true',
Expand Down
20 changes: 2 additions & 18 deletions src/types.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,5 @@
import type { Region } from '@saucelabs/testcomposer';

export type Browser = 'chromium' | 'firefox' | 'webkit' | 'chrome';

export interface Metrics {
name: string;
data: {
install: {duration: number};
rebuild?: {duration: number};
setup: {duration: number};
};
}

export interface RunResult {
startTime: string;
hasPassed: boolean;
endTime: string;
metrics: Metrics[];
}

export interface RunnerConfig {
// NOTE: Kind is serialized by saucectl with a capital 'K' ¯\_(ツ)_/¯
Kind: 'playwright';
Expand Down Expand Up @@ -59,6 +41,7 @@ export interface Suite {
env?: Record<string, string>;
preExec: string[];
testIgnore?: string[];
timeout?: number;
}

export interface SuiteConfig {
Expand Down Expand Up @@ -114,4 +97,5 @@ export interface CucumberSuite {
parallel?: number;
paths: string[];
};
timeout?: number;
}
3 changes: 1 addition & 2 deletions tests/fixtures/local/basic-js/sauce-runner.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,9 @@
"repeatEach": 1,
"retries": 0,
"shard": "1/2",
"timeout": 30000,
"maxFailures": 2
},
"testMatch": ".*.js"
}
]
}
}

0 comments on commit bde42ab

Please sign in to comment.