|
61 | 61 |
|
62 | 62 | var TEST_TIMEOUT_SECONDS = 45, |
63 | 63 | TEST_TIMEOUT = TEST_TIMEOUT_SECONDS * 1000, |
| 64 | + WORKER_STUCK_TIMEOUT = 120 * 1000, // 2 minutes without any activity |
64 | 65 | WORKER_NAME_PREFIX = "workerFrame", |
65 | 66 | busyCount = 0, |
66 | 67 | suitesDescription = { |
|
69 | 70 | version: "@(Model.Version)" |
70 | 71 | }, |
71 | 72 | suitesInProgress = [ ], |
| 73 | + workerLastActivity = [ ], // Track last activity time for each worker |
72 | 74 | urls = @Html.Raw(Json.Serialize(Model.Suites)), |
73 | 75 | originalUrls = urls.slice(0), |
74 | 76 | noTryCatch = @Html.Raw(Json.Serialize(Model.NoTryCatch)), |
|
87 | 89 | if(runWorkerInNewWindow) |
88 | 90 | return 1; // NOTE: more than 1 window cause problems with tests that use focus |
89 | 91 |
|
90 | | - var ua = navigator.userAgent; |
91 | | -
|
92 | | - if(window.ActiveXObject !== undefined) |
93 | | - return 1; |
94 | | -
|
95 | | - return 2; |
| 92 | + // Temporarily reduced to 1 worker to debug stalling issues |
| 93 | + return 4; // Was: return 2; |
96 | 94 | }; |
97 | 95 |
|
98 | 96 | WORKER_COUNT = calcWorkerFrameCount(); |
|
145 | 143 | var resultSaving = false; |
146 | 144 |
|
147 | 145 | var nextUrl = function(i) { |
| 146 | + console.log('nextUrl: worker=' + i + ', urls.length=' + urls.length + ', busyCount=' + busyCount + |
| 147 | + ', currentSuite=' + (suitesInProgress[i] ? suitesInProgress[i].name : 'null')); |
| 148 | + |
| 149 | + // Safety check: if worker is still busy, do not load next test |
| 150 | + if(suitesInProgress[i] !== null && suitesInProgress[i] !== undefined) { |
| 151 | + console.error('nextUrl: worker ' + i + ' is still busy with ' + suitesInProgress[i].name + '! Skipping.'); |
| 152 | + return; |
| 153 | + } |
| 154 | + |
148 | 155 | if(!urls.length) { |
| 156 | + console.log('nextUrl: No more URLs for worker ' + i); |
149 | 157 | // $.ajax(ROOT_URL + "run/something/FrameHasFinishedRunningASuite.js?frame=" + i); |
150 | 158 | if(!resultSaving && !busyCount) { |
| 159 | + console.log('All tests completed, saving results...'); |
151 | 160 | resultSaving = true; |
152 | 161 | rootSuite.time = roundTime((new Date() - rootStartTime) / 1000); |
153 | 162 | rootSuite.pureTime = roundTime(rootSuite.pureTime); |
154 | 163 | saveResults(); |
155 | 164 | window.onbeforeunload = $.noop; |
| 165 | + } else { |
| 166 | + console.log('Waiting for tests to complete: resultSaving=' + resultSaving + ', busyCount=' + busyCount); |
| 167 | + if(busyCount < 0) { |
| 168 | + console.error('ERROR: busyCount is negative! This should not happen.'); |
| 169 | + } |
| 170 | + |
| 171 | + // Debug: show which workers are still busy |
| 172 | + var busyWorkers = []; |
| 173 | + for(var w = 0; w < WORKER_COUNT; w++) { |
| 174 | + if(suitesInProgress[w]) { |
| 175 | + busyWorkers.push('Worker' + w + ':' + suitesInProgress[w].name); |
| 176 | + } |
| 177 | + } |
| 178 | + if(busyWorkers.length > 0) { |
| 179 | + console.log('Busy workers: ' + busyWorkers.join(', ')); |
| 180 | + } |
156 | 181 | } |
157 | 182 | return; |
158 | 183 | } |
|
191 | 216 |
|
192 | 217 | startTime: new Date(), |
193 | 218 | pureTime: 0, |
| 219 | + finalized: false, // Track if suite has been finalized |
194 | 220 |
|
195 | 221 | finalize: function(success) { |
| 222 | + if(this.finalized) { |
| 223 | + console.warn('Suite already finalized: ' + this.name + ', skipping duplicate finalize'); |
| 224 | + return; |
| 225 | + } |
| 226 | + |
| 227 | + var suiteName = this.name; // Save name before cleanup |
| 228 | + |
| 229 | + this.finalized = true; |
196 | 230 | this.time = roundTime((new Date() - this.startTime) / 1000); |
197 | 231 | this.pureTime = roundTime(this.pureTime); |
198 | 232 | delete this.startTime; |
|
203 | 237 | rootSuite.results.push(this); |
204 | 238 | suitesInProgress[i] = null; |
205 | 239 | busyCount--; |
206 | | -
|
207 | | - setTimeout(function() { nextUrl.call(that, _i); }, 0); |
| 240 | + |
| 241 | + console.log('Suite finalized: ' + suiteName + ', worker=' + _i + ', busyCount=' + busyCount + |
| 242 | + ', scheduling nextUrl in worker ' + _i); |
| 243 | +
|
| 244 | + setTimeout(function() { |
| 245 | + console.log('Calling nextUrl for worker ' + _i + ' after finalizing ' + suiteName); |
| 246 | + nextUrl.call(that, _i); |
| 247 | + }, 0); |
208 | 248 | } |
209 | 249 | }; |
210 | 250 |
|
211 | 251 | worker.name = WORKER_NAME_PREFIX + i; |
212 | | - worker.location = urlInfo.Url + "?" + $.param(additionalParams); |
213 | 252 | busyCount++; |
| 253 | + console.log('Loading test in worker ' + i + ': ' + urlInfo.FullName + ', busyCount=' + busyCount + |
| 254 | + ', urls remaining=' + urls.length); |
| 255 | + worker.location = urlInfo.Url + "?" + $.param(additionalParams); |
| 256 | + workerLastActivity[i] = Date.now(); // Mark worker as active |
214 | 257 | }; |
215 | 258 |
|
216 | 259 | var workers = [ ]; |
|
250 | 293 | return workers[index]; |
251 | 294 | }; |
252 | 295 |
|
| 296 | + var checkStuckWorkers = function() { |
| 297 | + var now = Date.now(); |
| 298 | + var activeWorkers = []; |
| 299 | + |
| 300 | + for(var i = 0; i < WORKER_COUNT; i++) { |
| 301 | + var lastActivity = workerLastActivity[i]; |
| 302 | + var suite = suitesInProgress[i]; |
| 303 | + |
| 304 | + if(suite && !suite.finalized) { |
| 305 | + var inactiveSeconds = Math.round((now - lastActivity) / 1000); |
| 306 | + activeWorkers.push('Worker' + i + ':' + suite.name + '(' + inactiveSeconds + 's)'); |
| 307 | + |
| 308 | + if(lastActivity && (now - lastActivity) > WORKER_STUCK_TIMEOUT) { |
| 309 | + console.error('Worker ' + i + ' is STUCK on test: ' + suite.name + ' (no activity for ' + |
| 310 | + inactiveSeconds + ' seconds), busyCount=' + busyCount); |
| 311 | + console.log('Force finalizing stuck worker ' + i); |
| 312 | + |
| 313 | + // Force finalize the stuck suite (finalize checks for double-finalization) |
| 314 | + suite.finalize(false); // Mark as failed |
| 315 | + |
| 316 | + // Reset worker state |
| 317 | + workerLastActivity[i] = now; |
| 318 | + } |
| 319 | + } |
| 320 | + } |
| 321 | + |
| 322 | + if(activeWorkers.length > 0 || busyCount > 0) { |
| 323 | + console.log('checkStuckWorkers: busyCount=' + busyCount + ', active: [' + activeWorkers.join(', ') + ']'); |
| 324 | + } |
| 325 | + }; |
| 326 | +
|
253 | 327 | var indexFromWorkerName = function(worker) { |
254 | 328 | return Number(worker.name.substr(WORKER_NAME_PREFIX.length)); |
255 | 329 | }; |
|
276 | 350 | var i = indexFromWorkerName(worker), |
277 | 351 | testSuite = suitesInProgress[i]; |
278 | 352 |
|
| 353 | + workerLastActivity[i] = Date.now(); // Mark worker activity |
279 | 354 | notifyIsAlive(); |
280 | 355 |
|
281 | 356 | var testCase = { |
|
329 | 404 | ); |
330 | 405 | } |
331 | 406 |
|
332 | | - $.post(@Html.Raw(Json.Serialize(Url.Action("NotifyTestStarted"))), { name: getTestCaseName(testSuite, qunitData) }); |
| 407 | + $.post(@Html.Raw(Json.Serialize(Url.Action("NotifyTestStarted"))), { name: getTestCaseName(testSuite, qunitData) }) |
| 408 | + .fail(function(jqXHR, textStatus, errorThrown) { |
| 409 | + console.warn('NotifyTestStarted failed:', textStatus, errorThrown); |
| 410 | + }); |
333 | 411 | }; |
334 | 412 |
|
335 | 413 | var indicateTestStatusInTitle = function(failed) { |
|
384 | 462 | testCases, |
385 | 463 | testCase; |
386 | 464 |
|
| 465 | + workerLastActivity[i] = Date.now(); // Mark worker activity |
| 466 | + |
387 | 467 | // Always notify on test done (removed throttling to prevent stalling) |
388 | 468 | notifyDeviceTestManager("QUnit.testCaseDone"); |
389 | 469 | notifyIsAlive(); |
|
416 | 496 | ); |
417 | 497 | } |
418 | 498 |
|
419 | | - $.post(@Html.Raw(Json.Serialize(Url.Action("NotifyTestCompleted"))), { name: getTestCaseName(testSuite, qunitData), passed: qunitData.passed === qunitData.total}); |
| 499 | + $.post(@Html.Raw(Json.Serialize(Url.Action("NotifyTestCompleted"))), { name: getTestCaseName(testSuite, qunitData), passed: qunitData.passed === qunitData.total}) |
| 500 | + .fail(function(jqXHR, textStatus, errorThrown) { |
| 501 | + console.warn('NotifyTestCompleted failed:', textStatus, errorThrown); |
| 502 | + }); |
420 | 503 | }; |
421 | 504 |
|
422 | 505 | window.RUNNER_ON_DONE = function(worker, qunitData) { |
423 | | - var suite = suitesInProgress[indexFromWorkerName(worker)], |
| 506 | + var i = indexFromWorkerName(worker), |
| 507 | + suite = suitesInProgress[i], |
424 | 508 | passed = !qunitData.failed; |
425 | 509 |
|
| 510 | + console.log('RUNNER_ON_DONE: worker=' + i + ', suite=' + (suite ? suite.name : 'null') + |
| 511 | + ', busyCount=' + busyCount + ', failed=' + qunitData.failed + ', total=' + qunitData.total); |
| 512 | +
|
426 | 513 | if(suite) { |
| 514 | + if(suite.finalized) { |
| 515 | + console.error('RUNNER_ON_DONE: suite ' + suite.name + ' is already finalized but still in suitesInProgress[' + i + ']!'); |
| 516 | + // This should not happen, but clean up just in case |
| 517 | + suitesInProgress[i] = null; |
| 518 | + return; // Do NOT call nextUrl - finalize() already did |
| 519 | + } |
| 520 | + |
| 521 | + var suiteName = suite.name; // Save before finalize clears it |
| 522 | + |
| 523 | + // finalize() handles busyCount-- internally and prevents double-finalization |
427 | 524 | suite.finalize(passed, qunitData.total); |
428 | | - notifySuiteFinalized(suite.name, passed, qunitData.runtime); |
| 525 | + notifySuiteFinalized(suiteName, passed, qunitData.runtime); |
| 526 | + } else { |
| 527 | + console.warn('RUNNER_ON_DONE: suite is null for worker ' + i + ' - already finalized, skipping nextUrl call'); |
| 528 | + // Suite is null - it was already finalized and nextUrl was already scheduled |
| 529 | + // Do NOT call nextUrl again to avoid loading tests twice in the same worker |
429 | 530 | } |
430 | 531 | }; |
431 | 532 |
|
|
459 | 560 | } |
460 | 561 |
|
461 | 562 | function notifySuiteFinalized(name, passed, runtime) { |
462 | | - $.post(@Html.Raw(Json.Serialize(Url.Action("NotifySuiteFinalized"))), { name: name, passed: passed, runtime: runtime }); |
| 563 | + $.post(@Html.Raw(Json.Serialize(Url.Action("NotifySuiteFinalized"))), { name: name, passed: passed, runtime: runtime }) |
| 564 | + .fail(function(jqXHR, textStatus, errorThrown) { |
| 565 | + console.warn('NotifySuiteFinalized failed:', textStatus, errorThrown); |
| 566 | + }); |
463 | 567 | } |
464 | 568 | function notifyIsAlive(){ |
465 | | - $.post(@Html.Raw(Json.Serialize(Url.Action("NotifyIsAlive")))); |
| 569 | + $.post(@Html.Raw(Json.Serialize(Url.Action("NotifyIsAlive")))) |
| 570 | + .fail(function(jqXHR, textStatus, errorThrown) { |
| 571 | + console.warn('NotifyIsAlive failed:', textStatus, errorThrown); |
| 572 | + }); |
466 | 573 | } |
467 | 574 |
|
| 575 | + // Check for stuck workers every 30 seconds |
| 576 | + setInterval(checkStuckWorkers, 30000); |
| 577 | +
|
468 | 578 | function roundTime(time) { |
469 | 579 | return +(time.toFixed(3)); |
470 | 580 | } |
|
0 commit comments