Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save muhqu/037477132ce7cbd208d13bbb84edfc7e to your computer and use it in GitHub Desktop.
Save muhqu/037477132ce7cbd208d13bbb84edfc7e to your computer and use it in GitHub Desktop.
Playwright 1.48.0 Improved Sharding

Playwright 1.48.0 Improved Sharding Patch

This patch includes the changes from microsoft/playwright#30962 (feat(test runner): improve sharding algorithm to better spread similar tests among shards)

Use patch-package to apply it on top of playwright v1.48.0.

Changes

This patch adds a new shardingMode configuration which allows to specify the sharding algorithm to be used…

shardingMode: 'partition'

That's the current behaviour, which is the default. Let me know if you have a better name to describe the current algorithm...

shardingMode: 'round-robin'

Distribute the test groups more evenly. It…

  1. sorts test groups by number of tests in descending order
  2. then loops through the test groups and assigns them to the shard with the lowest number of tests.

Here is a simple example where every test group represents a single test (e.g. --fully-parallel) ...

         [  1,  2,  3,  4,  5,  6,  7,  8,  9, 10, 11, 12]
Shard 1:    ^               ^               ^              : [  1, 5, 9 ]
Shard 2:        ^               ^               ^          : [  2, 6,10 ]
Shard 3:            ^               ^               ^      : [  3, 7,11 ]
Shard 4:                ^               ^               ^  : [  4, 8,12 ]

…or a more complex scenario where test groups have different number of tests…

Original Order: [ [1], [2, 3], [4, 5, 6], [7], [8], [9, 10], [11], [12] ]
Sorted Order:   [ [4, 5, 6], [2, 3], [9, 10], [1], [7], [8], [11], [12] ]
Shard 1:           ^-----^                                                : [ [ 4,   5,   6] ]
Shard 2:                      ^--^                       ^                : [ [ 2,  3],  [8] ]
Shard 3:                              ^---^                    ^          : [ [ 9, 10], [11] ]
Shard 4:                                       ^    ^                ^    : [ [1], [7], [12] ]

shardingMode: 'duration-round-robin'

It's very similar to round-robin, but it uses the duration of a tests previous run as cost factor. The duration will be read from .last-run.json when available. When a test can not be found in .last-run.json it will use the average duration of available tests. When no last run info is available, the behaviour would be identical to round-robin.

Other changes

  • Add testDurations?: { [testId: string]: number } to .last-run.json
  • Add builtin lastrun reporter, which allows merge-reports to generate a .last-run.json to be generated

Appendix

Below are some runtime stats from a project I've been working on, which shows the potential benefit of this change.

The tests runs had to complete 161 tests. Single test duration ranges from a few seconds to over 2 minutes.

image

The partition run gives the baseline performance and illustrates the problem quite good. We have a single shard that takes almost 16 min while another one completes in under 5 min.


image

The round-robin algorithm gives a bit better performance, but it still has a shard that requires twice the time of another shard.


image

The duration-round-robin run was using the duration info from a previous run and achieves the best result by far. All shards complete in 10-11 minutes. 🏆 🎉

diff --git a/node_modules/playwright/lib/common/config.js b/node_modules/playwright/lib/common/config.js
index dc2f6d7..2c81f2a 100644
--- a/node_modules/playwright/lib/common/config.js
+++ b/node_modules/playwright/lib/common/config.js
@@ -48,6 +48,8 @@ class FullConfigInternal {
this.cliLastFailed = void 0;
this.testIdMatcher = void 0;
this.defineConfigWasUsed = false;
+ this.shardingMode = void 0;
+ this.lastRunFile = void 0;
if (configCLIOverrides.projects && userConfig.projects) throw new Error(`Cannot use --browser option when configuration file defines projects. Specify browserName in the projects instead.`);
const {
resolvedConfigFile,
@@ -87,6 +89,8 @@ class FullConfigInternal {
workers: 0,
webServer: null
};
+ this.shardingMode = takeFirst(configCLIOverrides.shardingMode, userConfig.shardingMode, 'partition');
+ this.lastRunFile = configCLIOverrides.lastRunFile;
for (const key in userConfig) {
if (key.startsWith('@')) this.config[key] = userConfig[key];
}
diff --git a/node_modules/playwright/lib/common/configLoader.js b/node_modules/playwright/lib/common/configLoader.js
index 75fc987..177e249 100644
--- a/node_modules/playwright/lib/common/configLoader.js
+++ b/node_modules/playwright/lib/common/configLoader.js
@@ -192,6 +192,9 @@ function validateConfig(file, config) {
if (!('total' in config.shard) || typeof config.shard.total !== 'number' || config.shard.total < 1) throw (0, _util.errorWithFile)(file, `config.shard.total must be a positive number`);
if (!('current' in config.shard) || typeof config.shard.current !== 'number' || config.shard.current < 1 || config.shard.current > config.shard.total) throw (0, _util.errorWithFile)(file, `config.shard.current must be a positive number, not greater than config.shard.total`);
}
+ if ('shardingMode' in config && config.shardingMode !== undefined) {
+ if (typeof config.shardingMode !== 'string' || !['partition', 'round-robin', 'duration-round-robin'].includes(config.shardingMode)) throw (0, _util.errorWithFile)(file, `config.shardingMode must be one of "partition", "round-robin" or "duration-round-robin"`);
+ }
if ('updateSnapshots' in config && config.updateSnapshots !== undefined) {
if (typeof config.updateSnapshots !== 'string' || !['all', 'none', 'missing'].includes(config.updateSnapshots)) throw (0, _util.errorWithFile)(file, `config.updateSnapshots must be one of "all", "none" or "missing"`);
}
@@ -272,11 +275,11 @@ async function loadConfigFromFileRestartIfNeeded(configFile, overrides, ignoreDe
if (restartWithExperimentalTsEsm(location.resolvedConfigFile)) return null;
return await loadConfig(location, overrides, ignoreDeps);
}
-async function loadEmptyConfigForMergeReports() {
+async function loadEmptyConfigForMergeReports(overrides) {
// Merge reports is "different" for no good reason. It should not pick up local config from the cwd.
return await loadConfig({
configDir: process.cwd()
- });
+ }, overrides);
}
function restartWithExperimentalTsEsm(configFile, force = false) {
// Opt-out switch.
diff --git a/node_modules/playwright/lib/program.js b/node_modules/playwright/lib/program.js
index 19ebe36..642ab1f 100644
--- a/node_modules/playwright/lib/program.js
+++ b/node_modules/playwright/lib/program.js
@@ -156,6 +156,7 @@ function addMergeReportsCommand(program) {
});
command.option('-c, --config <file>', `Configuration file. Can be used to specify additional configuration for the output report.`);
command.option('--reporter <reporter>', `Reporter to use, comma-separated, can be ${_config.builtInReporters.map(name => `"${name}"`).join(', ')} (default: "${_config.defaultReporter}")`);
+ command.option('--last-run-file <file>', `Path to a json file where the last run information is written to (default: test-results/.last-run.json)`);
command.addHelpText('afterAll', `
Arguments [dir]:
Directory containing blob reports.
@@ -253,7 +254,8 @@ async function listTestFiles(opts) {
}
async function mergeReports(reportDir, opts) {
const configFile = opts.config;
- const config = configFile ? await (0, _configLoader.loadConfigFromFileRestartIfNeeded)(configFile) : await (0, _configLoader.loadEmptyConfigForMergeReports)();
+ const cliOverrides = overridesFromOptions(opts);
+ const config = configFile ? await (0, _configLoader.loadConfigFromFileRestartIfNeeded)(configFile, cliOverrides) : await (0, _configLoader.loadEmptyConfigForMergeReports)(cliOverrides);
if (!config) return;
const dir = _path.default.resolve(process.cwd(), reportDir || '');
const dirStat = await _fs.default.promises.stat(dir).catch(e => null);
@@ -282,6 +284,8 @@ function overridesFromOptions(options) {
current: shardPair[0],
total: shardPair[1]
} : undefined,
+ shardingMode: options.shardingMode ? resolveShardingModeOption(options.shardingMode) : undefined,
+ lastRunFile: options.lastRunFile ? _path.default.resolve(process.cwd(), options.lastRunFile) : undefined,
timeout: options.timeout ? parseInt(options.timeout, 10) : undefined,
tsconfig: options.tsconfig ? _path.default.resolve(process.cwd(), options.tsconfig) : undefined,
ignoreSnapshots: options.ignoreSnapshots ? !!options.ignoreSnapshots : undefined,
@@ -315,6 +319,12 @@ function overridesFromOptions(options) {
}
return overrides;
}
+const shardingModes = ['partition', 'round-robin', 'duration-round-robin'];
+function resolveShardingModeOption(shardingMode) {
+ if (!shardingMode) return undefined;
+ if (!shardingModes.includes(shardingMode)) throw new Error(`Unsupported sharding mode "${shardingMode}", must be one of: ${shardingModes.map(mode => `"${mode}"`).join(', ')}`);
+ return shardingMode;
+}
function resolveReporterOption(reporter) {
if (!reporter || !reporter.length) return undefined;
return reporter.split(',').map(r => [resolveReporter(r)]);
@@ -328,7 +338,7 @@ function resolveReporter(id) {
});
}
const kTraceModes = ['on', 'off', 'on-first-retry', 'on-all-retries', 'retain-on-failure', 'retain-on-first-failure'];
-const testOptions = [['--browser <browser>', `Browser to use for tests, one of "all", "chromium", "firefox" or "webkit" (default: "chromium")`], ['-c, --config <file>', `Configuration file, or a test directory with optional "playwright.config.{m,c}?{js,ts}"`], ['--debug', `Run tests with Playwright Inspector. Shortcut for "PWDEBUG=1" environment variable and "--timeout=0 --max-failures=1 --headed --workers=1" options`], ['--fail-on-flaky-tests', `Fail if any test is flagged as flaky (default: false)`], ['--forbid-only', `Fail if test.only is called (default: false)`], ['--fully-parallel', `Run all tests in parallel (default: false)`], ['--global-timeout <timeout>', `Maximum time this test suite can run in milliseconds (default: unlimited)`], ['-g, --grep <grep>', `Only run tests matching this regular expression (default: ".*")`], ['-gv, --grep-invert <grep>', `Only run tests that do not match this regular expression`], ['--headed', `Run tests in headed browsers (default: headless)`], ['--ignore-snapshots', `Ignore screenshot and snapshot expectations`], ['--last-failed', `Only re-run the failures`], ['--list', `Collect all the tests and report them, but do not run`], ['--max-failures <N>', `Stop after the first N failures`], ['--no-deps', 'Do not run project dependencies'], ['--output <dir>', `Folder for output artifacts (default: "test-results")`], ['--only-changed [ref]', `Only run test files that have been changed between 'HEAD' and 'ref'. Defaults to running all uncommitted changes. Only supports Git.`], ['--pass-with-no-tests', `Makes test run succeed even if no tests were found`], ['--project <project-name...>', `Only run tests from the specified list of projects, supports '*' wildcard (default: run all projects)`], ['--quiet', `Suppress stdio`], ['--repeat-each <N>', `Run each test N times (default: 1)`], ['--reporter <reporter>', `Reporter to use, comma-separated, can be ${_config.builtInReporters.map(name => `"${name}"`).join(', ')} (default: "${_config.defaultReporter}")`], ['--retries <retries>', `Maximum retry count for flaky tests, zero for no retries (default: no retries)`], ['--shard <shard>', `Shard tests and execute only the selected shard, specify in the form "current/all", 1-based, for example "3/5"`], ['--timeout <timeout>', `Specify test timeout threshold in milliseconds, zero for unlimited (default: ${_config.defaultTimeout})`], ['--trace <mode>', `Force tracing mode, can be ${kTraceModes.map(mode => `"${mode}"`).join(', ')}`], ['--tsconfig <path>', `Path to a single tsconfig applicable to all imported files (default: look up tsconfig for each imported file separately)`], ['--ui', `Run tests in interactive UI mode`], ['--ui-host <host>', 'Host to serve UI on; specifying this option opens UI in a browser tab'], ['--ui-port <port>', 'Port to serve UI on, 0 for any free port; specifying this option opens UI in a browser tab'], ['-u, --update-snapshots', `Update snapshots with actual results (default: only create missing snapshots)`], ['-j, --workers <workers>', `Number of concurrent workers or percentage of logical CPU cores, use 1 to run in a single worker (default: 50%)`], ['-x', `Stop after the first failure`]];
+const testOptions = [['--browser <browser>', `Browser to use for tests, one of "all", "chromium", "firefox" or "webkit" (default: "chromium")`], ['-c, --config <file>', `Configuration file, or a test directory with optional "playwright.config.{m,c}?{js,ts}"`], ['--debug', `Run tests with Playwright Inspector. Shortcut for "PWDEBUG=1" environment variable and "--timeout=0 --max-failures=1 --headed --workers=1" options`], ['--fail-on-flaky-tests', `Fail if any test is flagged as flaky (default: false)`], ['--forbid-only', `Fail if test.only is called (default: false)`], ['--fully-parallel', `Run all tests in parallel (default: false)`], ['--global-timeout <timeout>', `Maximum time this test suite can run in milliseconds (default: unlimited)`], ['-g, --grep <grep>', `Only run tests matching this regular expression (default: ".*")`], ['-gv, --grep-invert <grep>', `Only run tests that do not match this regular expression`], ['--headed', `Run tests in headed browsers (default: headless)`], ['--ignore-snapshots', `Ignore screenshot and snapshot expectations`], ['--last-failed', `Only re-run the failures`], ['--last-run-file <file>', `Path to a json file where the last run information is read from and written to (default: test-results/.last-run.json)`], ['--list', `Collect all the tests and report them, but do not run`], ['--max-failures <N>', `Stop after the first N failures`], ['--no-deps', 'Do not run project dependencies'], ['--output <dir>', `Folder for output artifacts (default: "test-results")`], ['--only-changed [ref]', `Only run test files that have been changed between 'HEAD' and 'ref'. Defaults to running all uncommitted changes. Only supports Git.`], ['--pass-with-no-tests', `Makes test run succeed even if no tests were found`], ['--project <project-name...>', `Only run tests from the specified list of projects, supports '*' wildcard (default: run all projects)`], ['--quiet', `Suppress stdio`], ['--repeat-each <N>', `Run each test N times (default: 1)`], ['--reporter <reporter>', `Reporter to use, comma-separated, can be ${_config.builtInReporters.map(name => `"${name}"`).join(', ')} (default: "${_config.defaultReporter}")`], ['--retries <retries>', `Maximum retry count for flaky tests, zero for no retries (default: no retries)`], ['--shard <shard>', `Shard tests and execute only the selected shard, specify in the form "current/all", 1-based, for example "3/5"`], ['--sharding-mode <mode>', `Sharding algorithm to use; "partition", "round-robin" or "duration-round-robin". Defaults to "partition".`], ['--timeout <timeout>', `Specify test timeout threshold in milliseconds, zero for unlimited (default: ${_config.defaultTimeout})`], ['--trace <mode>', `Force tracing mode, can be ${kTraceModes.map(mode => `"${mode}"`).join(', ')}`], ['--tsconfig <path>', `Path to a single tsconfig applicable to all imported files (default: look up tsconfig for each imported file separately)`], ['--ui', `Run tests in interactive UI mode`], ['--ui-host <host>', 'Host to serve UI on; specifying this option opens UI in a browser tab'], ['--ui-port <port>', 'Port to serve UI on, 0 for any free port; specifying this option opens UI in a browser tab'], ['-u, --update-snapshots', `Update snapshots with actual results (default: only create missing snapshots)`], ['-j, --workers <workers>', `Number of concurrent workers or percentage of logical CPU cores, use 1 to run in a single worker (default: 50%)`], ['-x', `Stop after the first failure`]];
addTestCommand(_program.program);
addShowReportCommand(_program.program);
addListFilesCommand(_program.program);
diff --git a/node_modules/playwright/lib/reporters/merge.js b/node_modules/playwright/lib/reporters/merge.js
index fca737b..d16141e 100644
--- a/node_modules/playwright/lib/reporters/merge.js
+++ b/node_modules/playwright/lib/reporters/merge.js
@@ -13,6 +13,7 @@ var _multiplexer = require("./multiplexer");
var _utils = require("playwright-core/lib/utils");
var _blob = require("./blob");
var _util = require("../util");
+var _lastRun = require("../runner/lastRun");
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
/**
* Copyright (c) Microsoft Corporation.
@@ -33,7 +34,8 @@ function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { de
async function createMergedReport(config, dir, reporterDescriptions, rootDirOverride) {
var _eventData$pathSepara;
const reporters = await (0, _reporters.createReporters)(config, 'merge', false, reporterDescriptions);
- const multiplexer = new _multiplexer.Multiplexer(reporters);
+ const lastRun = new _lastRun.LastRunReporter(config);
+ const multiplexer = new _multiplexer.Multiplexer([...reporters, lastRun]);
const stringPool = new _stringInternPool.StringInternPool();
let printStatus = () => {};
if (!multiplexer.printsToStdio()) {
diff --git a/node_modules/playwright/lib/runner/lastRun.js b/node_modules/playwright/lib/runner/lastRun.js
index d84abea..440a96b 100644
--- a/node_modules/playwright/lib/runner/lastRun.js
+++ b/node_modules/playwright/lib/runner/lastRun.js
@@ -30,16 +30,25 @@ class LastRunReporter {
this._lastRunFile = void 0;
this._suite = void 0;
this._config = config;
- const [project] = (0, _projectUtils.filterProjects)(config.projects, config.cliProjectFilter);
- if (project) this._lastRunFile = _path.default.join(project.project.outputDir, '.last-run.json');
+ if (config.lastRunFile) {
+ // specified via command line argument
+ this._lastRunFile = config.lastRunFile;
+ } else {
+ const [project] = (0, _projectUtils.filterProjects)(config.projects, config.cliProjectFilter);
+ if (project) this._lastRunFile = _path.default.join(project.project.outputDir, '.last-run.json');
+ }
}
- async filterLastFailed() {
+ async lastRunInfo() {
if (!this._lastRunFile) return;
try {
- const lastRunInfo = JSON.parse(await _fs.default.promises.readFile(this._lastRunFile, 'utf8'));
- this._config.testIdMatcher = id => lastRunInfo.failedTests.includes(id);
+ return JSON.parse(await _fs.default.promises.readFile(this._lastRunFile, 'utf8'));
} catch {}
}
+ async filterLastFailed() {
+ const lastRunInfo = await this.lastRunInfo();
+ if (!lastRunInfo) return;
+ this._config.testIdMatcher = id => lastRunInfo.failedTests.includes(id);
+ }
version() {
return 'v2';
}
@@ -50,15 +59,20 @@ class LastRunReporter {
this._suite = suite;
}
async onEnd(result) {
- var _this$_suite;
+ var _this$_suite, _this$_suite2;
if (!this._lastRunFile || this._config.cliListOnly) return;
await _fs.default.promises.mkdir(_path.default.dirname(this._lastRunFile), {
recursive: true
});
const failedTests = (_this$_suite = this._suite) === null || _this$_suite === void 0 ? void 0 : _this$_suite.allTests().filter(t => !t.ok()).map(t => t.id);
+ const testDurations = (_this$_suite2 = this._suite) === null || _this$_suite2 === void 0 ? void 0 : _this$_suite2.allTests().reduce((map, t) => {
+ map[t.id] = t.results.map(r => r.duration).reduce((a, b) => a + b, 0);
+ return map;
+ }, {});
const lastRunReport = JSON.stringify({
status: result.status,
- failedTests
+ failedTests,
+ testDurations
}, undefined, 2);
await _fs.default.promises.writeFile(this._lastRunFile, lastRunReport);
}
diff --git a/node_modules/playwright/lib/runner/loadUtils.js b/node_modules/playwright/lib/runner/loadUtils.js
index 8404253..e85d86c 100644
--- a/node_modules/playwright/lib/runner/loadUtils.js
+++ b/node_modules/playwright/lib/runner/loadUtils.js
@@ -182,7 +182,7 @@ async function createRootSuite(testRun, errors, shouldFilterOnly, additionalFile
for (const projectSuite of rootSuite.suites) testGroups.push(...(0, _testGroups.createTestGroups)(projectSuite, config.config.workers));
// Shard test groups.
- const testGroupsInThisShard = (0, _testGroups.filterForShard)(config.config.shard, testGroups);
+ const testGroupsInThisShard = await (0, _testGroups.filterForShard)(config, testGroups);
const testsInThisShard = new Set();
for (const group of testGroupsInThisShard) {
for (const test of group.tests) testsInThisShard.add(test);
diff --git a/node_modules/playwright/lib/runner/testGroups.js b/node_modules/playwright/lib/runner/testGroups.js
index 952aafe..58d8d31 100644
--- a/node_modules/playwright/lib/runner/testGroups.js
+++ b/node_modules/playwright/lib/runner/testGroups.js
@@ -5,6 +5,7 @@ Object.defineProperty(exports, "__esModule", {
});
exports.createTestGroups = createTestGroups;
exports.filterForShard = filterForShard;
+var _lastRun = require("./lastRun");
/**
* Copyright Microsoft Corporation. All rights reserved.
*
@@ -106,14 +107,36 @@ function createTestGroups(projectSuite, workers) {
}
return result;
}
-function filterForShard(shard, testGroups) {
+async function filterForShard(config, testGroups) {
// Note that sharding works based on test groups.
// This means parallel files will be sharded by single tests,
// while non-parallel files will be sharded by the whole file.
//
// Shards are still balanced by the number of tests, not files,
// even in the case of non-paralleled files.
+ const mode = config.shardingMode;
+ const shard = config.config.shard;
+ if (mode === 'round-robin') return filterForShardRoundRobin(shard, testGroups);
+ if (mode === 'duration-round-robin') {
+ const lastRun = new _lastRun.LastRunReporter(config);
+ const lastRunInfo = await lastRun.lastRunInfo();
+ return filterForShardRoundRobin(shard, testGroups, lastRunInfo);
+ }
+ return filterForShardPartition(shard, testGroups);
+}
+/**
+ * Shards tests by partitioning them into equal parts.
+ *
+ * ```
+ * [ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]
+ * Shard 1: ^---------^ : [ 1, 2, 3 ]
+ * Shard 2: ^---------^ : [ 4, 5, 6 ]
+ * Shard 3: ^---------^ : [ 7, 8, 9 ]
+ * Shard 4: ^---------^ : [ 10,11,12 ]
+ * ```
+ */
+function filterForShardPartition(shard, testGroups) {
let shardableTotal = 0;
for (const group of testGroups) shardableTotal += group.tests.length;
@@ -134,3 +157,41 @@ function filterForShard(shard, testGroups) {
}
return result;
}
+
+/**
+ * Shards tests by round-robin.
+ *
+ * ```
+ * [ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]
+ * Shard 1: ^ ^ ^ : [ 1, 5, 9 ]
+ * Shard 2: ^ ^ ^ : [ 2, 6,10 ]
+ * Shard 3: ^ ^ ^ : [ 3, 7,11 ]
+ * Shard 4: ^ ^ ^ : [ 4, 8,12 ]
+ * ```
+ */
+function filterForShardRoundRobin(shard, testGroups, lastRunInfo) {
+ const weights = new Array(shard.total).fill(0);
+ const shardSet = new Array(shard.total).fill(0).map(() => new Set());
+ const averageDuration = lastRunInfo ? Object.values((lastRunInfo === null || lastRunInfo === void 0 ? void 0 : lastRunInfo.testDurations) || {}).reduce((a, b) => a + b, 1) / Math.max(1, Object.values((lastRunInfo === null || lastRunInfo === void 0 ? void 0 : lastRunInfo.testDurations) || {}).length) : 0;
+ const weight = group => {
+ if (!lastRunInfo)
+ // If we don't have last run info, we just count the number of tests.
+ return group.tests.length;
+ // If we have last run info, we use the duration of the tests.
+ return group.tests.reduce((sum, test) => {
+ var _lastRunInfo$testDura;
+ return sum + Math.max(1, ((_lastRunInfo$testDura = lastRunInfo.testDurations) === null || _lastRunInfo$testDura === void 0 ? void 0 : _lastRunInfo$testDura[test.id]) || averageDuration);
+ }, 0);
+ };
+
+ // We sort the test groups by group duration in descending order.
+ const sortedTestGroups = testGroups.slice().sort((a, b) => weight(b) - weight(a));
+
+ // Then we add each group to the shard with the smallest number of tests.
+ for (const group of sortedTestGroups) {
+ const index = weights.reduce((minIndex, currentLength, currentIndex) => currentLength < weights[minIndex] ? currentIndex : minIndex, 0);
+ weights[index] += weight(group);
+ shardSet[index].add(group);
+ }
+ return shardSet[shard.current - 1];
+}
diff --git a/node_modules/playwright/types/test.d.ts b/node_modules/playwright/types/test.d.ts
index 400d7cb..c30ef7a 100644
--- a/node_modules/playwright/types/test.d.ts
+++ b/node_modules/playwright/types/test.d.ts
@@ -1407,7 +1407,7 @@ interface TestConfig<TestArgs = {}, WorkerArgs = {}> {
/**
* Shard tests and execute only the selected shard. Specify in the one-based form like `{ total: 5, current: 2 }`.
*
- * Learn more about [parallelism and sharding](https://playwright.dev/docs/test-parallel) with Playwright Test.
+ * Learn more about [parallelism](https://playwright.dev/docs/test-parallel) and [sharding](https://playwright.dev/docs/test-sharding) with Playwright Test.
*
* **Usage**
*
@@ -1433,6 +1433,21 @@ interface TestConfig<TestArgs = {}, WorkerArgs = {}> {
total: number;
};
+ /**
+ * Defines the algorithm to be used for sharding. Defaults to `'partition'`.
+ * - `'partition'` - divide the set of test groups by number of shards. e.g. first half goes to shard 1/2 and
+ * seconds half to shard 2/2.
+ * - `'round-robin'` - spread test groups to shards in a round-robin way. e.g. loop over test groups and always
+ * assign to the shard that has the lowest number of tests.
+ * - `'duration-round-robin'` - use duration info from `.last-run.json` to spread test groups to shards in a
+ * round-robin way. e.g. loop over test groups and always assign to the shard that has the lowest duration of
+ * tests. new tests which were not present in the last run will use an average duration time. When no
+ * `.last-run.json` could be found the behavior is identical to `'round-robin'`.
+ *
+ * Learn more about [sharding](https://playwright.dev/docs/test-sharding) with Playwright Test.
+ */
+ shardingMode?: "partition"|"round-robin"|"duration-round-robin";
+
/**
* **NOTE** Use
* [testConfig.snapshotPathTemplate](https://playwright.dev/docs/api/class-testconfig#test-config-snapshot-path-template)
@@ -5145,6 +5160,7 @@ export interface PlaywrightWorkerOptions {
video: VideoMode | /** deprecated */ 'retry-with-video' | { mode: VideoMode, size?: ViewportSize };
}
+export type ShardingMode = Exclude<PlaywrightTestConfig['shardingMode'], undefined>;
export type ScreenshotMode = 'off' | 'on' | 'only-on-failure';
export type TraceMode = 'off' | 'on' | 'retain-on-failure' | 'on-first-retry' | 'on-all-retries' | 'retain-on-first-failure';
export type VideoMode = 'off' | 'on' | 'retain-on-failure' | 'on-first-retry';
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment