Skip to content

[v3] feat: concurrent test execution via concurrency - #67

Open
monikon22 wants to merge 6 commits into
Drownek:v3-devfrom
monikon22:feat/64-concurrent-test-execution
Open

[v3] feat: concurrent test execution via concurrency#67
monikon22 wants to merge 6 commits into
Drownek:v3-devfrom
monikon22:feat/64-concurrent-test-execution

Conversation

@monikon22

Copy link
Copy Markdown
Contributor

Closes#64.

test() and describe.serial() had no way to put several bots on the same feature at once — everything runs as one instance, sequentially, awaited one at a time. Races between real players (two bots claiming the same chest, buying the last item in stock) never got exercised.

{ concurrency: N }

test('only one player can claim the chest',{concurrency: 3},async({ player })=>{player.chat('/claim');awaitexpect(player).toHaveReceivedMessage(/Claimed|alreadyclaimed/);});

N independent instances run at once, each with its own bot leased from the pool. One instance failing fails the whole result — this is for finding a race, not averaging a pass rate. describe.serial blocks take the same option and run N full copies of the ordered chain.

Concurrency is validated against AccountPool.capacity() before any test in the session runs (every spec file is loaded up front for this — see runner.ts), so concurrency: 10 against a 4-account pool fails immediately instead of the 5th bot hanging on a lease. LocalMode has no pool and nothing to check; its ceiling is max-players.

Two bugs concurrency exposed

  • session.consoleLog.clear() at the top of every test would have raced two concurrent tests wiping each other's messages. Replaced with a non-destructive cursor: ServerWrapper captures session.consoleLog.length at construction, and toHaveReceivedMessage defaults to reading from there instead of index 0.
  • createBotScope.close() called session.disconnectAllBots() with no arguments, tearing down every bot in the session — harmless sequentially, but the first concurrent instance to finish would have kicked every still-running sibling. Fixed to keep every bot outside its own scope.

Report shape

Still one row per test/test-in-block — concurrency multiplies bots, not report rows. durationMs is the slowest instance; a new instances array carries every instance's own outcome (bot username, pass/fail, duration), so a failure names which bot lost. Wired into the console summary and the JSON report; JUnit keeps the single aggregate.

Console log labeling

Test:, Serial block:, PASSED/FAILED, and bot-creation lines get an [i/N] tag — N instances logging the same test name at the same time was unreadable without it:

Test: concurrent bots each see their own marker and stay connected [1/3]
[Bot] Creating bot: pw_9c27 [1/3]
Test: concurrent bots each see their own marker and stay connected [2/3]
[Bot] Creating bot: pw_dfc2 [2/3]
Test: concurrent bots each see their own marker and stay connected [3/3]
[Bot] Creating bot: pw_8c5d [3/3]
...
PASSED [1/3] (2.5s)
PASSED [2/3] (2.9s)
PASSED [3/3] (3.0s)

Docs

Writing Tests gets a new section on concurrency; Reports gets the aggregated JSON shape.

Checked

tsc --noEmit clean. plugwrightTestLocal: 54/54 pass, run three times across the changes.

session.consoleLog.clear() wiped the whole shared buffer at the start of
every test. Fine when only one test runs at a time, but it means two
tests running concurrently would race to wipe each other's messages
out from under them.
ServerWrapper now captures its own startIndex at construction (and can
resetCursor() to "now"), and the toHaveReceivedMessage matcher defaults
to reading from that instead of index 0 when since isn't given. Same
observable behavior for a solo test, but the log itself is never
destroyed, so nothing racing to read it can lose messages.
test(name, { concurrency: N }, fn) and describe.serial(name, { concurrency: N }, fn)
fan out into N independent instances running at once, each with its
own bot leased from the account pool. One failing instance fails the
whole result — this is for races between real players, not a
pass-rate to average.
- Whole-session preflight: every spec file is loaded (imported once,
registrations snapshotted) before any test runs, and every
concurrency value is checked against the account pool's capacity()
up front. A misconfigured concurrency aborts immediately instead of
the Nth lease() hanging mid-run. An environment with no pool
(LocalMode mints a throwaway account per bot) has nothing to check.
- createBotScope.close() used to call session.disconnectAllBots()
with no arguments, tearing down every bot in the session rather than
just its own scope's. Harmless when nothing ran concurrently; with
concurrency it would mean one finishing instance kicking every
still-running sibling's bot. Fixed to keep every bot outside its own
scope.
- The report still has one row per test/test-in-block. Its durationMs
is the slowest instance, and a new instances array carries every
instance's own outcome (bot username, pass/fail, duration) so a
failure names which bot lost the race. Wired into the console
summary and the JSON report; JUnit keeps the single aggregated
pass/fail, no per-instance breakdown.
- A concurrent describe.serial block runs N full copies of the block;
instances can diverge mid-block (one loses its race and stops early
while another keeps going), so a test position only counts as
skipped if every instance skipped it.
- Console log lines (Test:, Serial block:, PASSED/FAILED, bot
creation) are tagged with [i/N] — N instances logging the same test
name at the same time is unreadable without it.
Covers both shapes: a plain test with concurrency, and a concurrent
describe.serial block. Each instance checks its own marker against the
shared server log and confirms it's still connected afterward — the
two bugs concurrency exposed (destructive log clear, scope-wide bot
teardown) would show up here as flaky or crashed instances.
Writing Tests gets a new section covering test()/describe.serial with
concurrency: what it's for, what you get back, how many instances you
can ask for, and why the server log stays shared and unfiltered across
instances. Reports gets the aggregated JSON shape (instances array,
botUsername) and a note that JUnit only ever sees the one aggregate.
…ut isn't full
expect(server).toHaveReceivedMessage() needs consoleOutput: 'full'. The
stand environment's RCON console only offers 'responses', same
limitation simple-ts.spec.ts's 'server logs command execution' already
declares — this test needed the same requires and didn't have it,
so it failed test-example-plugin-stand in CI instead of skipping.
Two additions, both surfacing data the aggregate result already had:
- TestInstanceResult gets index (1-based, matching the [i/N] console
log tag for that same run) — was missing from both the console
detail breakdown and the JSON report.
- The summary table's own row for a concurrent test now tags itself
with [passed/total] next to the duration, instead of that count
only showing up in the Failed Tests detail section below. A passing
concurrent test previously gave no indication in the table that it
was even concurrent.
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@monikon22
, 'i'); if (__m === '*' || __re.test(location.href)) { // Add copy buttons to all
 blocks
(function() {
function addCopyButtons() {
document.querySelectorAll('pre code').forEach(function(codeBlock) {
if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;
codeBlock.parentElement.setAttribute('data-copy-added', 'true');
var btn = document.createElement('button');
btn.textContent = 'Copy';
btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';
btn.onmouseover = function() { this.style.opacity = '1'; };
btn.onmouseout = function() { this.style.opacity = '0.7'; };
btn.onclick = function() {
navigator.clipboard.writeText(codeBlock.textContent).then(function() {
btn.textContent = 'Copied!';
setTimeout(function() { btn.textContent = 'Copy'; }, 1500);
});
};
codeBlock.parentElement.style.position = 'relative';
codeBlock.parentElement.appendChild(btn);
});
}
addCopyButtons();
// Re-run on dynamic content
var observer = new MutationObserver(addCopyButtons);
observer.observe(document.body, { childList: true, subtree: true });
})();
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
[v3] feat: concurrent test execution via `concurrency` by monikon22 · Pull Request #67 · Drownek/plugwright · GitHub
Skip to content

[v3] feat: concurrent test execution via concurrency - #67

Open
monikon22 wants to merge 6 commits into
Drownek:v3-devfrom
monikon22:feat/64-concurrent-test-execution
Open

[v3] feat: concurrent test execution via concurrency#67
monikon22 wants to merge 6 commits into
Drownek:v3-devfrom
monikon22:feat/64-concurrent-test-execution

Conversation

@monikon22

Copy link
Copy Markdown
Contributor

Closes#64.

test() and describe.serial() had no way to put several bots on the same feature at once — everything runs as one instance, sequentially, awaited one at a time. Races between real players (two bots claiming the same chest, buying the last item in stock) never got exercised.

{ concurrency: N }

test('only one player can claim the chest',{concurrency: 3},async({ player })=>{player.chat('/claim');awaitexpect(player).toHaveReceivedMessage(/Claimed|alreadyclaimed/);});

N independent instances run at once, each with its own bot leased from the pool. One instance failing fails the whole result — this is for finding a race, not averaging a pass rate. describe.serial blocks take the same option and run N full copies of the ordered chain.

Concurrency is validated against AccountPool.capacity() before any test in the session runs (every spec file is loaded up front for this — see runner.ts), so concurrency: 10 against a 4-account pool fails immediately instead of the 5th bot hanging on a lease. LocalMode has no pool and nothing to check; its ceiling is max-players.

Two bugs concurrency exposed

  • session.consoleLog.clear() at the top of every test would have raced two concurrent tests wiping each other's messages. Replaced with a non-destructive cursor: ServerWrapper captures session.consoleLog.length at construction, and toHaveReceivedMessage defaults to reading from there instead of index 0.
  • createBotScope.close() called session.disconnectAllBots() with no arguments, tearing down every bot in the session — harmless sequentially, but the first concurrent instance to finish would have kicked every still-running sibling. Fixed to keep every bot outside its own scope.

Report shape

Still one row per test/test-in-block — concurrency multiplies bots, not report rows. durationMs is the slowest instance; a new instances array carries every instance's own outcome (bot username, pass/fail, duration), so a failure names which bot lost. Wired into the console summary and the JSON report; JUnit keeps the single aggregate.

Console log labeling

Test:, Serial block:, PASSED/FAILED, and bot-creation lines get an [i/N] tag — N instances logging the same test name at the same time was unreadable without it:

Test: concurrent bots each see their own marker and stay connected [1/3]
[Bot] Creating bot: pw_9c27 [1/3]
Test: concurrent bots each see their own marker and stay connected [2/3]
[Bot] Creating bot: pw_dfc2 [2/3]
Test: concurrent bots each see their own marker and stay connected [3/3]
[Bot] Creating bot: pw_8c5d [3/3]
...
PASSED [1/3] (2.5s)
PASSED [2/3] (2.9s)
PASSED [3/3] (3.0s)

Docs

Writing Tests gets a new section on concurrency; Reports gets the aggregated JSON shape.

Checked

tsc --noEmit clean. plugwrightTestLocal: 54/54 pass, run three times across the changes.

session.consoleLog.clear() wiped the whole shared buffer at the start of
every test. Fine when only one test runs at a time, but it means two
tests running concurrently would race to wipe each other's messages
out from under them.
ServerWrapper now captures its own startIndex at construction (and can
resetCursor() to "now"), and the toHaveReceivedMessage matcher defaults
to reading from that instead of index 0 when since isn't given. Same
observable behavior for a solo test, but the log itself is never
destroyed, so nothing racing to read it can lose messages.
test(name, { concurrency: N }, fn) and describe.serial(name, { concurrency: N }, fn)
fan out into N independent instances running at once, each with its
own bot leased from the account pool. One failing instance fails the
whole result — this is for races between real players, not a
pass-rate to average.
- Whole-session preflight: every spec file is loaded (imported once,
registrations snapshotted) before any test runs, and every
concurrency value is checked against the account pool's capacity()
up front. A misconfigured concurrency aborts immediately instead of
the Nth lease() hanging mid-run. An environment with no pool
(LocalMode mints a throwaway account per bot) has nothing to check.
- createBotScope.close() used to call session.disconnectAllBots()
with no arguments, tearing down every bot in the session rather than
just its own scope's. Harmless when nothing ran concurrently; with
concurrency it would mean one finishing instance kicking every
still-running sibling's bot. Fixed to keep every bot outside its own
scope.
- The report still has one row per test/test-in-block. Its durationMs
is the slowest instance, and a new instances array carries every
instance's own outcome (bot username, pass/fail, duration) so a
failure names which bot lost the race. Wired into the console
summary and the JSON report; JUnit keeps the single aggregated
pass/fail, no per-instance breakdown.
- A concurrent describe.serial block runs N full copies of the block;
instances can diverge mid-block (one loses its race and stops early
while another keeps going), so a test position only counts as
skipped if every instance skipped it.
- Console log lines (Test:, Serial block:, PASSED/FAILED, bot
creation) are tagged with [i/N] — N instances logging the same test
name at the same time is unreadable without it.
Covers both shapes: a plain test with concurrency, and a concurrent
describe.serial block. Each instance checks its own marker against the
shared server log and confirms it's still connected afterward — the
two bugs concurrency exposed (destructive log clear, scope-wide bot
teardown) would show up here as flaky or crashed instances.
Writing Tests gets a new section covering test()/describe.serial with
concurrency: what it's for, what you get back, how many instances you
can ask for, and why the server log stays shared and unfiltered across
instances. Reports gets the aggregated JSON shape (instances array,
botUsername) and a note that JUnit only ever sees the one aggregate.
…ut isn't full
expect(server).toHaveReceivedMessage() needs consoleOutput: 'full'. The
stand environment's RCON console only offers 'responses', same
limitation simple-ts.spec.ts's 'server logs command execution' already
declares — this test needed the same requires and didn't have it,
so it failed test-example-plugin-stand in CI instead of skipping.
Two additions, both surfacing data the aggregate result already had:
- TestInstanceResult gets index (1-based, matching the [i/N] console
log tag for that same run) — was missing from both the console
detail breakdown and the JSON report.
- The summary table's own row for a concurrent test now tags itself
with [passed/total] next to the duration, instead of that count
only showing up in the Failed Tests detail section below. A passing
concurrent test previously gave no indication in the table that it
was even concurrent.
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@monikon22
, 'i'); if (__m === '*' || __re.test(location.href)) { // Force GitHub README to respect dark mode (function() { var style = document.createElement('style'); style.textContent = ' .markdown-body { color-scheme: dark light; } .markdown-body pre { background: #161b22 !important; } .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; } .markdown-body table th, .markdown-body table td { border-color: #30363d !important; } .markdown-body img { background: #0d1117; } .markdown-body blockquote { border-left-color: #8b949e; } .markdown-body hr { border-color: #30363d; } '; document.head.appendChild(style); })(); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' [v3] feat: concurrent test execution via `concurrency` by monikon22 · Pull Request #67 · Drownek/plugwright · GitHub
Skip to content

[v3] feat: concurrent test execution via concurrency - #67

Open
monikon22 wants to merge 6 commits into
Drownek:v3-devfrom
monikon22:feat/64-concurrent-test-execution
Open

[v3] feat: concurrent test execution via concurrency#67
monikon22 wants to merge 6 commits into
Drownek:v3-devfrom
monikon22:feat/64-concurrent-test-execution

Conversation

@monikon22

Copy link
Copy Markdown
Contributor

Closes#64.

test() and describe.serial() had no way to put several bots on the same feature at once — everything runs as one instance, sequentially, awaited one at a time. Races between real players (two bots claiming the same chest, buying the last item in stock) never got exercised.

{ concurrency: N }

test('only one player can claim the chest',{concurrency: 3},async({ player })=>{player.chat('/claim');awaitexpect(player).toHaveReceivedMessage(/Claimed|alreadyclaimed/);});

N independent instances run at once, each with its own bot leased from the pool. One instance failing fails the whole result — this is for finding a race, not averaging a pass rate. describe.serial blocks take the same option and run N full copies of the ordered chain.

Concurrency is validated against AccountPool.capacity() before any test in the session runs (every spec file is loaded up front for this — see runner.ts), so concurrency: 10 against a 4-account pool fails immediately instead of the 5th bot hanging on a lease. LocalMode has no pool and nothing to check; its ceiling is max-players.

Two bugs concurrency exposed

  • session.consoleLog.clear() at the top of every test would have raced two concurrent tests wiping each other's messages. Replaced with a non-destructive cursor: ServerWrapper captures session.consoleLog.length at construction, and toHaveReceivedMessage defaults to reading from there instead of index 0.
  • createBotScope.close() called session.disconnectAllBots() with no arguments, tearing down every bot in the session — harmless sequentially, but the first concurrent instance to finish would have kicked every still-running sibling. Fixed to keep every bot outside its own scope.

Report shape

Still one row per test/test-in-block — concurrency multiplies bots, not report rows. durationMs is the slowest instance; a new instances array carries every instance's own outcome (bot username, pass/fail, duration), so a failure names which bot lost. Wired into the console summary and the JSON report; JUnit keeps the single aggregate.

Console log labeling

Test:, Serial block:, PASSED/FAILED, and bot-creation lines get an [i/N] tag — N instances logging the same test name at the same time was unreadable without it:

Test: concurrent bots each see their own marker and stay connected [1/3]
[Bot] Creating bot: pw_9c27 [1/3]
Test: concurrent bots each see their own marker and stay connected [2/3]
[Bot] Creating bot: pw_dfc2 [2/3]
Test: concurrent bots each see their own marker and stay connected [3/3]
[Bot] Creating bot: pw_8c5d [3/3]
...
PASSED [1/3] (2.5s)
PASSED [2/3] (2.9s)
PASSED [3/3] (3.0s)

Docs

Writing Tests gets a new section on concurrency; Reports gets the aggregated JSON shape.

Checked

tsc --noEmit clean. plugwrightTestLocal: 54/54 pass, run three times across the changes.

session.consoleLog.clear() wiped the whole shared buffer at the start of
every test. Fine when only one test runs at a time, but it means two
tests running concurrently would race to wipe each other's messages
out from under them.
ServerWrapper now captures its own startIndex at construction (and can
resetCursor() to "now"), and the toHaveReceivedMessage matcher defaults
to reading from that instead of index 0 when since isn't given. Same
observable behavior for a solo test, but the log itself is never
destroyed, so nothing racing to read it can lose messages.
test(name, { concurrency: N }, fn) and describe.serial(name, { concurrency: N }, fn)
fan out into N independent instances running at once, each with its
own bot leased from the account pool. One failing instance fails the
whole result — this is for races between real players, not a
pass-rate to average.
- Whole-session preflight: every spec file is loaded (imported once,
registrations snapshotted) before any test runs, and every
concurrency value is checked against the account pool's capacity()
up front. A misconfigured concurrency aborts immediately instead of
the Nth lease() hanging mid-run. An environment with no pool
(LocalMode mints a throwaway account per bot) has nothing to check.
- createBotScope.close() used to call session.disconnectAllBots()
with no arguments, tearing down every bot in the session rather than
just its own scope's. Harmless when nothing ran concurrently; with
concurrency it would mean one finishing instance kicking every
still-running sibling's bot. Fixed to keep every bot outside its own
scope.
- The report still has one row per test/test-in-block. Its durationMs
is the slowest instance, and a new instances array carries every
instance's own outcome (bot username, pass/fail, duration) so a
failure names which bot lost the race. Wired into the console
summary and the JSON report; JUnit keeps the single aggregated
pass/fail, no per-instance breakdown.
- A concurrent describe.serial block runs N full copies of the block;
instances can diverge mid-block (one loses its race and stops early
while another keeps going), so a test position only counts as
skipped if every instance skipped it.
- Console log lines (Test:, Serial block:, PASSED/FAILED, bot
creation) are tagged with [i/N] — N instances logging the same test
name at the same time is unreadable without it.
Covers both shapes: a plain test with concurrency, and a concurrent
describe.serial block. Each instance checks its own marker against the
shared server log and confirms it's still connected afterward — the
two bugs concurrency exposed (destructive log clear, scope-wide bot
teardown) would show up here as flaky or crashed instances.
Writing Tests gets a new section covering test()/describe.serial with
concurrency: what it's for, what you get back, how many instances you
can ask for, and why the server log stays shared and unfiltered across
instances. Reports gets the aggregated JSON shape (instances array,
botUsername) and a note that JUnit only ever sees the one aggregate.
…ut isn't full
expect(server).toHaveReceivedMessage() needs consoleOutput: 'full'. The
stand environment's RCON console only offers 'responses', same
limitation simple-ts.spec.ts's 'server logs command execution' already
declares — this test needed the same requires and didn't have it,
so it failed test-example-plugin-stand in CI instead of skipping.
Two additions, both surfacing data the aggregate result already had:
- TestInstanceResult gets index (1-based, matching the [i/N] console
log tag for that same run) — was missing from both the console
detail breakdown and the JSON report.
- The summary table's own row for a concurrent test now tags itself
with [passed/total] next to the duration, instead of that count
only showing up in the Failed Tests detail section below. A passing
concurrent test previously gave no indication in the table that it
was even concurrent.
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@monikon22
, 'i'); if (__m === '*' || __re.test(location.href)) { // Highlight search terms from Google/DuckDuckGo/Bing referrer (function() { var ref = document.referrer; var terms = []; if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) { var url = new URL(ref); var q = url.searchParams.get('q') || url.searchParams.get('p'); if (q) { terms = q.split(/\s+/).filter(function(t) { return t.length > 2; }); } } if (terms.length === 0) return; var style = document.createElement('style'); style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }'; document.head.appendChild(style); function highlight(node) { if (node.nodeType === 3) { // text node var text = node.textContent; var found = false; terms.forEach(function(term) { var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\]\\]/g, '\\') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' [v3] feat: concurrent test execution via `concurrency` by monikon22 · Pull Request #67 · Drownek/plugwright · GitHub
Skip to content

[v3] feat: concurrent test execution via concurrency - #67

Open
monikon22 wants to merge 6 commits into
Drownek:v3-devfrom
monikon22:feat/64-concurrent-test-execution
Open

[v3] feat: concurrent test execution via concurrency#67
monikon22 wants to merge 6 commits into
Drownek:v3-devfrom
monikon22:feat/64-concurrent-test-execution

Conversation

@monikon22

Copy link
Copy Markdown
Contributor

Closes#64.

test() and describe.serial() had no way to put several bots on the same feature at once — everything runs as one instance, sequentially, awaited one at a time. Races between real players (two bots claiming the same chest, buying the last item in stock) never got exercised.

{ concurrency: N }

test('only one player can claim the chest',{concurrency: 3},async({ player })=>{player.chat('/claim');awaitexpect(player).toHaveReceivedMessage(/Claimed|alreadyclaimed/);});

N independent instances run at once, each with its own bot leased from the pool. One instance failing fails the whole result — this is for finding a race, not averaging a pass rate. describe.serial blocks take the same option and run N full copies of the ordered chain.

Concurrency is validated against AccountPool.capacity() before any test in the session runs (every spec file is loaded up front for this — see runner.ts), so concurrency: 10 against a 4-account pool fails immediately instead of the 5th bot hanging on a lease. LocalMode has no pool and nothing to check; its ceiling is max-players.

Two bugs concurrency exposed

  • session.consoleLog.clear() at the top of every test would have raced two concurrent tests wiping each other's messages. Replaced with a non-destructive cursor: ServerWrapper captures session.consoleLog.length at construction, and toHaveReceivedMessage defaults to reading from there instead of index 0.
  • createBotScope.close() called session.disconnectAllBots() with no arguments, tearing down every bot in the session — harmless sequentially, but the first concurrent instance to finish would have kicked every still-running sibling. Fixed to keep every bot outside its own scope.

Report shape

Still one row per test/test-in-block — concurrency multiplies bots, not report rows. durationMs is the slowest instance; a new instances array carries every instance's own outcome (bot username, pass/fail, duration), so a failure names which bot lost. Wired into the console summary and the JSON report; JUnit keeps the single aggregate.

Console log labeling

Test:, Serial block:, PASSED/FAILED, and bot-creation lines get an [i/N] tag — N instances logging the same test name at the same time was unreadable without it:

Test: concurrent bots each see their own marker and stay connected [1/3]
[Bot] Creating bot: pw_9c27 [1/3]
Test: concurrent bots each see their own marker and stay connected [2/3]
[Bot] Creating bot: pw_dfc2 [2/3]
Test: concurrent bots each see their own marker and stay connected [3/3]
[Bot] Creating bot: pw_8c5d [3/3]
...
PASSED [1/3] (2.5s)
PASSED [2/3] (2.9s)
PASSED [3/3] (3.0s)

Docs

Writing Tests gets a new section on concurrency; Reports gets the aggregated JSON shape.

Checked

tsc --noEmit clean. plugwrightTestLocal: 54/54 pass, run three times across the changes.

session.consoleLog.clear() wiped the whole shared buffer at the start of
every test. Fine when only one test runs at a time, but it means two
tests running concurrently would race to wipe each other's messages
out from under them.
ServerWrapper now captures its own startIndex at construction (and can
resetCursor() to "now"), and the toHaveReceivedMessage matcher defaults
to reading from that instead of index 0 when since isn't given. Same
observable behavior for a solo test, but the log itself is never
destroyed, so nothing racing to read it can lose messages.
test(name, { concurrency: N }, fn) and describe.serial(name, { concurrency: N }, fn)
fan out into N independent instances running at once, each with its
own bot leased from the account pool. One failing instance fails the
whole result — this is for races between real players, not a
pass-rate to average.
- Whole-session preflight: every spec file is loaded (imported once,
registrations snapshotted) before any test runs, and every
concurrency value is checked against the account pool's capacity()
up front. A misconfigured concurrency aborts immediately instead of
the Nth lease() hanging mid-run. An environment with no pool
(LocalMode mints a throwaway account per bot) has nothing to check.
- createBotScope.close() used to call session.disconnectAllBots()
with no arguments, tearing down every bot in the session rather than
just its own scope's. Harmless when nothing ran concurrently; with
concurrency it would mean one finishing instance kicking every
still-running sibling's bot. Fixed to keep every bot outside its own
scope.
- The report still has one row per test/test-in-block. Its durationMs
is the slowest instance, and a new instances array carries every
instance's own outcome (bot username, pass/fail, duration) so a
failure names which bot lost the race. Wired into the console
summary and the JSON report; JUnit keeps the single aggregated
pass/fail, no per-instance breakdown.
- A concurrent describe.serial block runs N full copies of the block;
instances can diverge mid-block (one loses its race and stops early
while another keeps going), so a test position only counts as
skipped if every instance skipped it.
- Console log lines (Test:, Serial block:, PASSED/FAILED, bot
creation) are tagged with [i/N] — N instances logging the same test
name at the same time is unreadable without it.
Covers both shapes: a plain test with concurrency, and a concurrent
describe.serial block. Each instance checks its own marker against the
shared server log and confirms it's still connected afterward — the
two bugs concurrency exposed (destructive log clear, scope-wide bot
teardown) would show up here as flaky or crashed instances.
Writing Tests gets a new section covering test()/describe.serial with
concurrency: what it's for, what you get back, how many instances you
can ask for, and why the server log stays shared and unfiltered across
instances. Reports gets the aggregated JSON shape (instances array,
botUsername) and a note that JUnit only ever sees the one aggregate.
…ut isn't full
expect(server).toHaveReceivedMessage() needs consoleOutput: 'full'. The
stand environment's RCON console only offers 'responses', same
limitation simple-ts.spec.ts's 'server logs command execution' already
declares — this test needed the same requires and didn't have it,
so it failed test-example-plugin-stand in CI instead of skipping.
Two additions, both surfacing data the aggregate result already had:
- TestInstanceResult gets index (1-based, matching the [i/N] console
log tag for that same run) — was missing from both the console
detail breakdown and the JSON report.
- The summary table's own row for a concurrent test now tags itself
with [passed/total] next to the duration, instead of that count
only showing up in the Failed Tests detail section below. A passing
concurrent test previously gave no indication in the table that it
was even concurrent.
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@monikon22
, 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + ' [v3] feat: concurrent test execution via `concurrency` by monikon22 · Pull Request #67 · Drownek/plugwright · GitHub
Skip to content

[v3] feat: concurrent test execution via concurrency - #67

Open
monikon22 wants to merge 6 commits into
Drownek:v3-devfrom
monikon22:feat/64-concurrent-test-execution
Open

[v3] feat: concurrent test execution via concurrency#67
monikon22 wants to merge 6 commits into
Drownek:v3-devfrom
monikon22:feat/64-concurrent-test-execution

Conversation

@monikon22

Copy link
Copy Markdown
Contributor

Closes#64.

test() and describe.serial() had no way to put several bots on the same feature at once — everything runs as one instance, sequentially, awaited one at a time. Races between real players (two bots claiming the same chest, buying the last item in stock) never got exercised.

{ concurrency: N }

test('only one player can claim the chest',{concurrency: 3},async({ player })=>{player.chat('/claim');awaitexpect(player).toHaveReceivedMessage(/Claimed|alreadyclaimed/);});

N independent instances run at once, each with its own bot leased from the pool. One instance failing fails the whole result — this is for finding a race, not averaging a pass rate. describe.serial blocks take the same option and run N full copies of the ordered chain.

Concurrency is validated against AccountPool.capacity() before any test in the session runs (every spec file is loaded up front for this — see runner.ts), so concurrency: 10 against a 4-account pool fails immediately instead of the 5th bot hanging on a lease. LocalMode has no pool and nothing to check; its ceiling is max-players.

Two bugs concurrency exposed

  • session.consoleLog.clear() at the top of every test would have raced two concurrent tests wiping each other's messages. Replaced with a non-destructive cursor: ServerWrapper captures session.consoleLog.length at construction, and toHaveReceivedMessage defaults to reading from there instead of index 0.
  • createBotScope.close() called session.disconnectAllBots() with no arguments, tearing down every bot in the session — harmless sequentially, but the first concurrent instance to finish would have kicked every still-running sibling. Fixed to keep every bot outside its own scope.

Report shape

Still one row per test/test-in-block — concurrency multiplies bots, not report rows. durationMs is the slowest instance; a new instances array carries every instance's own outcome (bot username, pass/fail, duration), so a failure names which bot lost. Wired into the console summary and the JSON report; JUnit keeps the single aggregate.

Console log labeling

Test:, Serial block:, PASSED/FAILED, and bot-creation lines get an [i/N] tag — N instances logging the same test name at the same time was unreadable without it:

Test: concurrent bots each see their own marker and stay connected [1/3]
[Bot] Creating bot: pw_9c27 [1/3]
Test: concurrent bots each see their own marker and stay connected [2/3]
[Bot] Creating bot: pw_dfc2 [2/3]
Test: concurrent bots each see their own marker and stay connected [3/3]
[Bot] Creating bot: pw_8c5d [3/3]
...
PASSED [1/3] (2.5s)
PASSED [2/3] (2.9s)
PASSED [3/3] (3.0s)

Docs

Writing Tests gets a new section on concurrency; Reports gets the aggregated JSON shape.

Checked

tsc --noEmit clean. plugwrightTestLocal: 54/54 pass, run three times across the changes.

session.consoleLog.clear() wiped the whole shared buffer at the start of
every test. Fine when only one test runs at a time, but it means two
tests running concurrently would race to wipe each other's messages
out from under them.
ServerWrapper now captures its own startIndex at construction (and can
resetCursor() to "now"), and the toHaveReceivedMessage matcher defaults
to reading from that instead of index 0 when since isn't given. Same
observable behavior for a solo test, but the log itself is never
destroyed, so nothing racing to read it can lose messages.
test(name, { concurrency: N }, fn) and describe.serial(name, { concurrency: N }, fn)
fan out into N independent instances running at once, each with its
own bot leased from the account pool. One failing instance fails the
whole result — this is for races between real players, not a
pass-rate to average.
- Whole-session preflight: every spec file is loaded (imported once,
registrations snapshotted) before any test runs, and every
concurrency value is checked against the account pool's capacity()
up front. A misconfigured concurrency aborts immediately instead of
the Nth lease() hanging mid-run. An environment with no pool
(LocalMode mints a throwaway account per bot) has nothing to check.
- createBotScope.close() used to call session.disconnectAllBots()
with no arguments, tearing down every bot in the session rather than
just its own scope's. Harmless when nothing ran concurrently; with
concurrency it would mean one finishing instance kicking every
still-running sibling's bot. Fixed to keep every bot outside its own
scope.
- The report still has one row per test/test-in-block. Its durationMs
is the slowest instance, and a new instances array carries every
instance's own outcome (bot username, pass/fail, duration) so a
failure names which bot lost the race. Wired into the console
summary and the JSON report; JUnit keeps the single aggregated
pass/fail, no per-instance breakdown.
- A concurrent describe.serial block runs N full copies of the block;
instances can diverge mid-block (one loses its race and stops early
while another keeps going), so a test position only counts as
skipped if every instance skipped it.
- Console log lines (Test:, Serial block:, PASSED/FAILED, bot
creation) are tagged with [i/N] — N instances logging the same test
name at the same time is unreadable without it.
Covers both shapes: a plain test with concurrency, and a concurrent
describe.serial block. Each instance checks its own marker against the
shared server log and confirms it's still connected afterward — the
two bugs concurrency exposed (destructive log clear, scope-wide bot
teardown) would show up here as flaky or crashed instances.
Writing Tests gets a new section covering test()/describe.serial with
concurrency: what it's for, what you get back, how many instances you
can ask for, and why the server log stays shared and unfiltered across
instances. Reports gets the aggregated JSON shape (instances array,
botUsername) and a note that JUnit only ever sees the one aggregate.
…ut isn't full
expect(server).toHaveReceivedMessage() needs consoleOutput: 'full'. The
stand environment's RCON console only offers 'responses', same
limitation simple-ts.spec.ts's 'server logs command execution' already
declares — this test needed the same requires and didn't have it,
so it failed test-example-plugin-stand in CI instead of skipping.
Two additions, both surfacing data the aggregate result already had:
- TestInstanceResult gets index (1-based, matching the [i/N] console
log tag for that same run) — was missing from both the console
detail breakdown and the JSON report.
- The summary table's own row for a concurrent test now tags itself
with [passed/total] next to the duration, instead of that count
only showing up in the Failed Tests detail section below. A passing
concurrent test previously gave no indication in the table that it
was even concurrent.
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@monikon22
, 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' [v3] feat: concurrent test execution via `concurrency` by monikon22 · Pull Request #67 · Drownek/plugwright · GitHub
Skip to content

[v3] feat: concurrent test execution via concurrency - #67

Open
monikon22 wants to merge 6 commits into
Drownek:v3-devfrom
monikon22:feat/64-concurrent-test-execution
Open

[v3] feat: concurrent test execution via concurrency#67
monikon22 wants to merge 6 commits into
Drownek:v3-devfrom
monikon22:feat/64-concurrent-test-execution

Conversation

@monikon22

Copy link
Copy Markdown
Contributor

Closes#64.

test() and describe.serial() had no way to put several bots on the same feature at once — everything runs as one instance, sequentially, awaited one at a time. Races between real players (two bots claiming the same chest, buying the last item in stock) never got exercised.

{ concurrency: N }

test('only one player can claim the chest',{concurrency: 3},async({ player })=>{player.chat('/claim');awaitexpect(player).toHaveReceivedMessage(/Claimed|alreadyclaimed/);});

N independent instances run at once, each with its own bot leased from the pool. One instance failing fails the whole result — this is for finding a race, not averaging a pass rate. describe.serial blocks take the same option and run N full copies of the ordered chain.

Concurrency is validated against AccountPool.capacity() before any test in the session runs (every spec file is loaded up front for this — see runner.ts), so concurrency: 10 against a 4-account pool fails immediately instead of the 5th bot hanging on a lease. LocalMode has no pool and nothing to check; its ceiling is max-players.

Two bugs concurrency exposed

  • session.consoleLog.clear() at the top of every test would have raced two concurrent tests wiping each other's messages. Replaced with a non-destructive cursor: ServerWrapper captures session.consoleLog.length at construction, and toHaveReceivedMessage defaults to reading from there instead of index 0.
  • createBotScope.close() called session.disconnectAllBots() with no arguments, tearing down every bot in the session — harmless sequentially, but the first concurrent instance to finish would have kicked every still-running sibling. Fixed to keep every bot outside its own scope.

Report shape

Still one row per test/test-in-block — concurrency multiplies bots, not report rows. durationMs is the slowest instance; a new instances array carries every instance's own outcome (bot username, pass/fail, duration), so a failure names which bot lost. Wired into the console summary and the JSON report; JUnit keeps the single aggregate.

Console log labeling

Test:, Serial block:, PASSED/FAILED, and bot-creation lines get an [i/N] tag — N instances logging the same test name at the same time was unreadable without it:

Test: concurrent bots each see their own marker and stay connected [1/3]
[Bot] Creating bot: pw_9c27 [1/3]
Test: concurrent bots each see their own marker and stay connected [2/3]
[Bot] Creating bot: pw_dfc2 [2/3]
Test: concurrent bots each see their own marker and stay connected [3/3]
[Bot] Creating bot: pw_8c5d [3/3]
...
PASSED [1/3] (2.5s)
PASSED [2/3] (2.9s)
PASSED [3/3] (3.0s)

Docs

Writing Tests gets a new section on concurrency; Reports gets the aggregated JSON shape.

Checked

tsc --noEmit clean. plugwrightTestLocal: 54/54 pass, run three times across the changes.

session.consoleLog.clear() wiped the whole shared buffer at the start of
every test. Fine when only one test runs at a time, but it means two
tests running concurrently would race to wipe each other's messages
out from under them.
ServerWrapper now captures its own startIndex at construction (and can
resetCursor() to "now"), and the toHaveReceivedMessage matcher defaults
to reading from that instead of index 0 when since isn't given. Same
observable behavior for a solo test, but the log itself is never
destroyed, so nothing racing to read it can lose messages.
test(name, { concurrency: N }, fn) and describe.serial(name, { concurrency: N }, fn)
fan out into N independent instances running at once, each with its
own bot leased from the account pool. One failing instance fails the
whole result — this is for races between real players, not a
pass-rate to average.
- Whole-session preflight: every spec file is loaded (imported once,
registrations snapshotted) before any test runs, and every
concurrency value is checked against the account pool's capacity()
up front. A misconfigured concurrency aborts immediately instead of
the Nth lease() hanging mid-run. An environment with no pool
(LocalMode mints a throwaway account per bot) has nothing to check.
- createBotScope.close() used to call session.disconnectAllBots()
with no arguments, tearing down every bot in the session rather than
just its own scope's. Harmless when nothing ran concurrently; with
concurrency it would mean one finishing instance kicking every
still-running sibling's bot. Fixed to keep every bot outside its own
scope.
- The report still has one row per test/test-in-block. Its durationMs
is the slowest instance, and a new instances array carries every
instance's own outcome (bot username, pass/fail, duration) so a
failure names which bot lost the race. Wired into the console
summary and the JSON report; JUnit keeps the single aggregated
pass/fail, no per-instance breakdown.
- A concurrent describe.serial block runs N full copies of the block;
instances can diverge mid-block (one loses its race and stops early
while another keeps going), so a test position only counts as
skipped if every instance skipped it.
- Console log lines (Test:, Serial block:, PASSED/FAILED, bot
creation) are tagged with [i/N] — N instances logging the same test
name at the same time is unreadable without it.
Covers both shapes: a plain test with concurrency, and a concurrent
describe.serial block. Each instance checks its own marker against the
shared server log and confirms it's still connected afterward — the
two bugs concurrency exposed (destructive log clear, scope-wide bot
teardown) would show up here as flaky or crashed instances.
Writing Tests gets a new section covering test()/describe.serial with
concurrency: what it's for, what you get back, how many instances you
can ask for, and why the server log stays shared and unfiltered across
instances. Reports gets the aggregated JSON shape (instances array,
botUsername) and a note that JUnit only ever sees the one aggregate.
…ut isn't full
expect(server).toHaveReceivedMessage() needs consoleOutput: 'full'. The
stand environment's RCON console only offers 'responses', same
limitation simple-ts.spec.ts's 'server logs command execution' already
declares — this test needed the same requires and didn't have it,
so it failed test-example-plugin-stand in CI instead of skipping.
Two additions, both surfacing data the aggregate result already had:
- TestInstanceResult gets index (1-based, matching the [i/N] console
log tag for that same run) — was missing from both the console
detail breakdown and the JSON report.
- The summary table's own row for a concurrent test now tags itself
with [passed/total] next to the duration, instead of that count
only showing up in the Failed Tests detail section below. A passing
concurrent test previously gave no indication in the table that it
was even concurrent.
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@monikon22
, 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' [v3] feat: concurrent test execution via `concurrency` by monikon22 · Pull Request #67 · Drownek/plugwright · GitHub
Skip to content

[v3] feat: concurrent test execution via concurrency - #67

Open
monikon22 wants to merge 6 commits into
Drownek:v3-devfrom
monikon22:feat/64-concurrent-test-execution
Open

[v3] feat: concurrent test execution via concurrency#67
monikon22 wants to merge 6 commits into
Drownek:v3-devfrom
monikon22:feat/64-concurrent-test-execution

Conversation

@monikon22

Copy link
Copy Markdown
Contributor

Closes#64.

test() and describe.serial() had no way to put several bots on the same feature at once — everything runs as one instance, sequentially, awaited one at a time. Races between real players (two bots claiming the same chest, buying the last item in stock) never got exercised.

{ concurrency: N }

test('only one player can claim the chest',{concurrency: 3},async({ player })=>{player.chat('/claim');awaitexpect(player).toHaveReceivedMessage(/Claimed|alreadyclaimed/);});

N independent instances run at once, each with its own bot leased from the pool. One instance failing fails the whole result — this is for finding a race, not averaging a pass rate. describe.serial blocks take the same option and run N full copies of the ordered chain.

Concurrency is validated against AccountPool.capacity() before any test in the session runs (every spec file is loaded up front for this — see runner.ts), so concurrency: 10 against a 4-account pool fails immediately instead of the 5th bot hanging on a lease. LocalMode has no pool and nothing to check; its ceiling is max-players.

Two bugs concurrency exposed

  • session.consoleLog.clear() at the top of every test would have raced two concurrent tests wiping each other's messages. Replaced with a non-destructive cursor: ServerWrapper captures session.consoleLog.length at construction, and toHaveReceivedMessage defaults to reading from there instead of index 0.
  • createBotScope.close() called session.disconnectAllBots() with no arguments, tearing down every bot in the session — harmless sequentially, but the first concurrent instance to finish would have kicked every still-running sibling. Fixed to keep every bot outside its own scope.

Report shape

Still one row per test/test-in-block — concurrency multiplies bots, not report rows. durationMs is the slowest instance; a new instances array carries every instance's own outcome (bot username, pass/fail, duration), so a failure names which bot lost. Wired into the console summary and the JSON report; JUnit keeps the single aggregate.

Console log labeling

Test:, Serial block:, PASSED/FAILED, and bot-creation lines get an [i/N] tag — N instances logging the same test name at the same time was unreadable without it:

Test: concurrent bots each see their own marker and stay connected [1/3]
[Bot] Creating bot: pw_9c27 [1/3]
Test: concurrent bots each see their own marker and stay connected [2/3]
[Bot] Creating bot: pw_dfc2 [2/3]
Test: concurrent bots each see their own marker and stay connected [3/3]
[Bot] Creating bot: pw_8c5d [3/3]
...
PASSED [1/3] (2.5s)
PASSED [2/3] (2.9s)
PASSED [3/3] (3.0s)

Docs

Writing Tests gets a new section on concurrency; Reports gets the aggregated JSON shape.

Checked

tsc --noEmit clean. plugwrightTestLocal: 54/54 pass, run three times across the changes.

session.consoleLog.clear() wiped the whole shared buffer at the start of
every test. Fine when only one test runs at a time, but it means two
tests running concurrently would race to wipe each other's messages
out from under them.
ServerWrapper now captures its own startIndex at construction (and can
resetCursor() to "now"), and the toHaveReceivedMessage matcher defaults
to reading from that instead of index 0 when since isn't given. Same
observable behavior for a solo test, but the log itself is never
destroyed, so nothing racing to read it can lose messages.
test(name, { concurrency: N }, fn) and describe.serial(name, { concurrency: N }, fn)
fan out into N independent instances running at once, each with its
own bot leased from the account pool. One failing instance fails the
whole result — this is for races between real players, not a
pass-rate to average.
- Whole-session preflight: every spec file is loaded (imported once,
registrations snapshotted) before any test runs, and every
concurrency value is checked against the account pool's capacity()
up front. A misconfigured concurrency aborts immediately instead of
the Nth lease() hanging mid-run. An environment with no pool
(LocalMode mints a throwaway account per bot) has nothing to check.
- createBotScope.close() used to call session.disconnectAllBots()
with no arguments, tearing down every bot in the session rather than
just its own scope's. Harmless when nothing ran concurrently; with
concurrency it would mean one finishing instance kicking every
still-running sibling's bot. Fixed to keep every bot outside its own
scope.
- The report still has one row per test/test-in-block. Its durationMs
is the slowest instance, and a new instances array carries every
instance's own outcome (bot username, pass/fail, duration) so a
failure names which bot lost the race. Wired into the console
summary and the JSON report; JUnit keeps the single aggregated
pass/fail, no per-instance breakdown.
- A concurrent describe.serial block runs N full copies of the block;
instances can diverge mid-block (one loses its race and stops early
while another keeps going), so a test position only counts as
skipped if every instance skipped it.
- Console log lines (Test:, Serial block:, PASSED/FAILED, bot
creation) are tagged with [i/N] — N instances logging the same test
name at the same time is unreadable without it.
Covers both shapes: a plain test with concurrency, and a concurrent
describe.serial block. Each instance checks its own marker against the
shared server log and confirms it's still connected afterward — the
two bugs concurrency exposed (destructive log clear, scope-wide bot
teardown) would show up here as flaky or crashed instances.
Writing Tests gets a new section covering test()/describe.serial with
concurrency: what it's for, what you get back, how many instances you
can ask for, and why the server log stays shared and unfiltered across
instances. Reports gets the aggregated JSON shape (instances array,
botUsername) and a note that JUnit only ever sees the one aggregate.
…ut isn't full
expect(server).toHaveReceivedMessage() needs consoleOutput: 'full'. The
stand environment's RCON console only offers 'responses', same
limitation simple-ts.spec.ts's 'server logs command execution' already
declares — this test needed the same requires and didn't have it,
so it failed test-example-plugin-stand in CI instead of skipping.
Two additions, both surfacing data the aggregate result already had:
- TestInstanceResult gets index (1-based, matching the [i/N] console
log tag for that same run) — was missing from both the console
detail breakdown and the JSON report.
- The summary table's own row for a concurrent test now tags itself
with [passed/total] next to the duration, instead of that count
only showing up in the Failed Tests detail section below. A passing
concurrent test previously gave no indication in the table that it
was even concurrent.
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@monikon22
, 'i'); if (__m === '*' || __re.test(location.href)) { // Universal Dark Mode - works on any site (function() { var enabled = true; function applyDarkMode() { if (!enabled) return; // Create style element if it doesn't exist var style = document.getElementById('universal-dark-mode-style'); if (!style) { style = document.createElement('style'); style.id = 'universal-dark-mode-style'; document.head.appendChild(style); } // Dark mode CSS - inverts colors but preserves images/video style.textContent = ' /* Invert everything except media */ html { filter: invert(1) hue-rotate(180deg) !important; background: #1a1a2e !important; } /* Restore images, videos, iframes, canvas */ img, video, iframe, canvas, svg, picture, [style*="background-image"] { filter: invert(1) hue-rotate(180deg) !important; } /* Preserve specific elements that should not be inverted */ .no-dark-mode, .no-dark-mode *, [data-theme="light"], [data-theme="light"], .ace_editor, .ace_editor *, .CodeMirror, .CodeMirror *, .monaco-editor, .monaco-editor *, .markdown-body pre, .markdown-body pre *, .highlight, .highlight *, pre code, pre code * { filter: none !important; } /* Fix common UI elements */ .modal, .popup, .dropdown-menu, .tooltip, .popover { filter: invert(1) hue-rotate(180deg) !important; background: #2d2d44 !important; border-color: #444 !important; } /* Scrollbars */ ::-webkit-scrollbar { background: #1a1a2e !important; } ::-webkit-scrollbar-thumb { background: #444 !important; } ::-webkit-scrollbar-thumb:hover { background: #555 !important; } /* Selection */ ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; } ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; } '; } function removeDarkMode() { var style = document.getElementById('universal-dark-mode-style'); if (style) style.remove(); } // Toggle with Alt+Shift+D document.addEventListener('keydown', function(e) { if (e.altKey && e.shiftKey && e.key === 'D') { e.preventDefault(); enabled = !enabled; if (enabled) { applyDarkMode(); console.log('[Universal Dark Mode] Enabled'); } else { removeDarkMode(); console.log('[Universal Dark Mode] Disabled'); } } }); // Apply on load applyDarkMode(); // Re-apply on dynamic content var observer = new MutationObserver(function(mutations) { if (enabled && !document.getElementById('universal-dark-mode-style')) { applyDarkMode(); } }); observer.observe(document.head, { childList: true }); console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle'); })(); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })(); [v3] feat: concurrent test execution via `concurrency` by monikon22 · Pull Request #67 · Drownek/plugwright · GitHub
Skip to content

[v3] feat: concurrent test execution via concurrency - #67

Open
monikon22 wants to merge 6 commits into
Drownek:v3-devfrom
monikon22:feat/64-concurrent-test-execution
Open

[v3] feat: concurrent test execution via concurrency#67
monikon22 wants to merge 6 commits into
Drownek:v3-devfrom
monikon22:feat/64-concurrent-test-execution

Conversation

@monikon22

Copy link
Copy Markdown
Contributor

Closes#64.

test() and describe.serial() had no way to put several bots on the same feature at once — everything runs as one instance, sequentially, awaited one at a time. Races between real players (two bots claiming the same chest, buying the last item in stock) never got exercised.

{ concurrency: N }

test('only one player can claim the chest',{concurrency: 3},async({ player })=>{player.chat('/claim');awaitexpect(player).toHaveReceivedMessage(/Claimed|alreadyclaimed/);});

N independent instances run at once, each with its own bot leased from the pool. One instance failing fails the whole result — this is for finding a race, not averaging a pass rate. describe.serial blocks take the same option and run N full copies of the ordered chain.

Concurrency is validated against AccountPool.capacity() before any test in the session runs (every spec file is loaded up front for this — see runner.ts), so concurrency: 10 against a 4-account pool fails immediately instead of the 5th bot hanging on a lease. LocalMode has no pool and nothing to check; its ceiling is max-players.

Two bugs concurrency exposed

  • session.consoleLog.clear() at the top of every test would have raced two concurrent tests wiping each other's messages. Replaced with a non-destructive cursor: ServerWrapper captures session.consoleLog.length at construction, and toHaveReceivedMessage defaults to reading from there instead of index 0.
  • createBotScope.close() called session.disconnectAllBots() with no arguments, tearing down every bot in the session — harmless sequentially, but the first concurrent instance to finish would have kicked every still-running sibling. Fixed to keep every bot outside its own scope.

Report shape

Still one row per test/test-in-block — concurrency multiplies bots, not report rows. durationMs is the slowest instance; a new instances array carries every instance's own outcome (bot username, pass/fail, duration), so a failure names which bot lost. Wired into the console summary and the JSON report; JUnit keeps the single aggregate.

Console log labeling

Test:, Serial block:, PASSED/FAILED, and bot-creation lines get an [i/N] tag — N instances logging the same test name at the same time was unreadable without it:

Test: concurrent bots each see their own marker and stay connected [1/3]
[Bot] Creating bot: pw_9c27 [1/3]
Test: concurrent bots each see their own marker and stay connected [2/3]
[Bot] Creating bot: pw_dfc2 [2/3]
Test: concurrent bots each see their own marker and stay connected [3/3]
[Bot] Creating bot: pw_8c5d [3/3]
...
PASSED [1/3] (2.5s)
PASSED [2/3] (2.9s)
PASSED [3/3] (3.0s)

Docs

Writing Tests gets a new section on concurrency; Reports gets the aggregated JSON shape.

Checked

tsc --noEmit clean. plugwrightTestLocal: 54/54 pass, run three times across the changes.

session.consoleLog.clear() wiped the whole shared buffer at the start of
every test. Fine when only one test runs at a time, but it means two
tests running concurrently would race to wipe each other's messages
out from under them.
ServerWrapper now captures its own startIndex at construction (and can
resetCursor() to "now"), and the toHaveReceivedMessage matcher defaults
to reading from that instead of index 0 when since isn't given. Same
observable behavior for a solo test, but the log itself is never
destroyed, so nothing racing to read it can lose messages.
test(name, { concurrency: N }, fn) and describe.serial(name, { concurrency: N }, fn)
fan out into N independent instances running at once, each with its
own bot leased from the account pool. One failing instance fails the
whole result — this is for races between real players, not a
pass-rate to average.
- Whole-session preflight: every spec file is loaded (imported once,
registrations snapshotted) before any test runs, and every
concurrency value is checked against the account pool's capacity()
up front. A misconfigured concurrency aborts immediately instead of
the Nth lease() hanging mid-run. An environment with no pool
(LocalMode mints a throwaway account per bot) has nothing to check.
- createBotScope.close() used to call session.disconnectAllBots()
with no arguments, tearing down every bot in the session rather than
just its own scope's. Harmless when nothing ran concurrently; with
concurrency it would mean one finishing instance kicking every
still-running sibling's bot. Fixed to keep every bot outside its own
scope.
- The report still has one row per test/test-in-block. Its durationMs
is the slowest instance, and a new instances array carries every
instance's own outcome (bot username, pass/fail, duration) so a
failure names which bot lost the race. Wired into the console
summary and the JSON report; JUnit keeps the single aggregated
pass/fail, no per-instance breakdown.
- A concurrent describe.serial block runs N full copies of the block;
instances can diverge mid-block (one loses its race and stops early
while another keeps going), so a test position only counts as
skipped if every instance skipped it.
- Console log lines (Test:, Serial block:, PASSED/FAILED, bot
creation) are tagged with [i/N] — N instances logging the same test
name at the same time is unreadable without it.
Covers both shapes: a plain test with concurrency, and a concurrent
describe.serial block. Each instance checks its own marker against the
shared server log and confirms it's still connected afterward — the
two bugs concurrency exposed (destructive log clear, scope-wide bot
teardown) would show up here as flaky or crashed instances.
Writing Tests gets a new section covering test()/describe.serial with
concurrency: what it's for, what you get back, how many instances you
can ask for, and why the server log stays shared and unfiltered across
instances. Reports gets the aggregated JSON shape (instances array,
botUsername) and a note that JUnit only ever sees the one aggregate.
…ut isn't full
expect(server).toHaveReceivedMessage() needs consoleOutput: 'full'. The
stand environment's RCON console only offers 'responses', same
limitation simple-ts.spec.ts's 'server logs command execution' already
declares — this test needed the same requires and didn't have it,
so it failed test-example-plugin-stand in CI instead of skipping.
Two additions, both surfacing data the aggregate result already had:
- TestInstanceResult gets index (1-based, matching the [i/N] console
log tag for that same run) — was missing from both the console
detail breakdown and the JSON report.
- The summary table's own row for a concurrent test now tags itself
with [passed/total] next to the duration, instead of that count
only showing up in the Failed Tests detail section below. A passing
concurrent test previously gave no indication in the table that it
was even concurrent.
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@monikon22