browser-automation wave 2: testcafe-0001 + webdriverio-0002

testcafe-0001: Selector filterNodes (string-filter branch) and
  expandSelectorResults both dedup via Array.indexOf on growing result
  arrays. filterNodes: O(N*M) per selector filter. expandSelectorResults:
  O(N^2 * K^2) worst case when derivatives unique. Fix: Set<Node> keyed
  by object identity. Bench: 398x at N=2000 filter, 1966x at N=K=150
  expand.

webdriverio-0002: MSPO aggregator dedups per-test entries via Array.find
  on growing bucket array. O(N^2) per test bucket, same pattern repeats
  in unknown-suite merger. Fix: companion Map<bucketKey, Set<selector>>
  for O(1) dedup. Bench: 493x at N=2000.

UNDF IDs: 1290 (testcafe), 1291 (webdriverio-0002). All 17 tests pass.
This commit is contained in:
russell@unturf.com 2026-04-22 18:31:53 -04:00
parent 9a0253e724
commit b79fddfb51
16 changed files with 827 additions and 17 deletions

View file

@ -0,0 +1,60 @@
# UNDF: UNDF-2026-000001290
# UNDF: UNDF-2026-XXXXXXXXX
# CWE-407: Algorithmic Complexity -- O(N*M) -> O(N+M) in Selector filter dedup,
# O(N^2*K^2) -> O(N*K) in derivative expansion
#
# Defect: filterNodes (string-filter branch) uses matchingArr.indexOf(node) > -1
# per input node, O(N*M). expandSelectorResults uses result.indexOf(deriv) < 0
# against a growing result array, worst case O(N^2*K^2) when all derivatives
# are unique. Both run as client functions injected into the test page;
# executed per Selector().filter() and per Selector().parent()/child()/etc.
#
# Fix: Replace array-indexOf dedup with Set membership test. Object identity
# semantics preserved (Set keys by object reference, same as indexOf on Node
# references). Total cost drops to O(N+M) and O(N*K) respectively.
#
# Complexity gate (tests/test-testcafe-cwe407.py):
# k-scaling 5x: time ratio must be <17.5x (O(k) ~=5x, not O(k^2) ~=25x)
# filterNodes N=M=1000: must complete in <5ms
# expandSelectorResults N=K=100: must complete in <5ms
--- a/src/client-functions/selectors/add-api.js
+++ b/src/client-functions/selectors/add-api.js
@@ -30,12 +30,12 @@ const filterNodes = new ClientFunctionBuilder((nodes, filter, querySelectorRoot,
return null;
const matching = querySelectorRoot.querySelectorAll(filter);
- const matchingArr = [];
-
+ // Set keyed by object identity gives O(1) membership test; prior impl
+ // used Array.indexOf(node) > -1 inside a per-node loop, giving O(N*M).
+ const matchingSet = new Set();
for (let i = 0; i < matching.length; i++)
- matchingArr.push(matching[i]);
-
- filter = node => matchingArr.indexOf(node) > -1;
+ matchingSet.add(matching[i]);
+ filter = node => matchingSet.has(node);
}
if (typeof filter === 'function') {
@@ -55,13 +55,16 @@ const expandSelectorResults = new ClientFunctionBuilder((selector, populateDeriv
const result = [];
+ // Parallel Set tracks which derivative Nodes have been pushed; prior impl
+ // used result.indexOf(deriv) < 0, giving O(N*K*|result|) worst case.
+ const seen = new Set();
+
for (let i = 0; i < nodes.length; i++) {
const derivativeNodes = populateDerivativeNodes(nodes[i]);
if (derivativeNodes) {
for (let j = 0; j < derivativeNodes.length; j++) {
- if (result.indexOf(derivativeNodes[j]) < 0)
+ if (!seen.has(derivativeNodes[j])) {
+ seen.add(derivativeNodes[j]);
result.push(derivativeNodes[j]);
+ }
}
}
}