|
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)), |
|
92 | 94 | if(window.ActiveXObject !== undefined) |
93 | 95 | return 1; |
94 | 96 |
|
95 | | - return 2; |
| 97 | + // Temporarily reduced to 1 worker to debug stalling issues |
| 98 | + return 4; // Was: return 2; |
96 | 99 | }; |
97 | 100 |
|
98 | 101 | WORKER_COUNT = calcWorkerFrameCount(); |
|
145 | 148 | var resultSaving = false; |
146 | 149 |
|
147 | 150 | var nextUrl = function(i) { |
| 151 | + console.log('nextUrl: worker=' + i + ', urls.length=' + urls.length + ', busyCount=' + busyCount + |
| 152 | + ', currentSuite=' + (suitesInProgress[i] ? suitesInProgress[i].name : 'null')); |
| 153 | + |
148 | 154 | if(!urls.length) { |
149 | 155 | // $.ajax(ROOT_URL + "run/something/FrameHasFinishedRunningASuite.js?frame=" + i); |
150 | 156 | if(!resultSaving && !busyCount) { |
| 157 | + console.log('All tests completed, saving results...'); |
151 | 158 | resultSaving = true; |
152 | 159 | rootSuite.time = roundTime((new Date() - rootStartTime) / 1000); |
153 | 160 | rootSuite.pureTime = roundTime(rootSuite.pureTime); |
154 | 161 | saveResults(); |
155 | 162 | window.onbeforeunload = $.noop; |
| 163 | + } else { |
| 164 | + console.log('Waiting for tests to complete: resultSaving=' + resultSaving + ', busyCount=' + busyCount); |
| 165 | + if(busyCount < 0) { |
| 166 | + console.error('ERROR: busyCount is negative! This should not happen.'); |
| 167 | + } |
156 | 168 | } |
157 | 169 | return; |
158 | 170 | } |
|
191 | 203 |
|
192 | 204 | startTime: new Date(), |
193 | 205 | pureTime: 0, |
| 206 | + finalized: false, // Track if suite has been finalized |
194 | 207 |
|
195 | 208 | finalize: function(success) { |
| 209 | + if(this.finalized) { |
| 210 | + console.warn('Suite already finalized: ' + this.name + ', skipping duplicate finalize'); |
| 211 | + return; |
| 212 | + } |
| 213 | + |
| 214 | + this.finalized = true; |
196 | 215 | this.time = roundTime((new Date() - this.startTime) / 1000); |
197 | 216 | this.pureTime = roundTime(this.pureTime); |
198 | 217 | delete this.startTime; |
|
203 | 222 | rootSuite.results.push(this); |
204 | 223 | suitesInProgress[i] = null; |
205 | 224 | busyCount--; |
| 225 | + |
| 226 | + console.log('Suite finalized: ' + this.name + ', busyCount=' + busyCount); |
206 | 227 |
|
207 | 228 | setTimeout(function() { nextUrl.call(that, _i); }, 0); |
208 | 229 | } |
209 | 230 | }; |
210 | 231 |
|
211 | 232 | worker.name = WORKER_NAME_PREFIX + i; |
212 | | - worker.location = urlInfo.Url + "?" + $.param(additionalParams); |
213 | 233 | busyCount++; |
| 234 | + console.log('Loading test in worker ' + i + ': ' + urlInfo.FullName + ', busyCount=' + busyCount); |
| 235 | + worker.location = urlInfo.Url + "?" + $.param(additionalParams); |
| 236 | + workerLastActivity[i] = Date.now(); // Mark worker as active |
214 | 237 | }; |
215 | 238 |
|
216 | 239 | var workers = [ ]; |
|
250 | 273 | return workers[index]; |
251 | 274 | }; |
252 | 275 |
|
| 276 | + var checkStuckWorkers = function() { |
| 277 | + var now = Date.now(); |
| 278 | + for(var i = 0; i < WORKER_COUNT; i++) { |
| 279 | + var lastActivity = workerLastActivity[i]; |
| 280 | + var suite = suitesInProgress[i]; |
| 281 | + |
| 282 | + if(suite && !suite.finalized && lastActivity && (now - lastActivity) > WORKER_STUCK_TIMEOUT) { |
| 283 | + console.error('Worker ' + i + ' is stuck on test: ' + suite.name + ' (no activity for ' + |
| 284 | + Math.round((now - lastActivity) / 1000) + ' seconds), busyCount=' + busyCount); |
| 285 | + console.log('Force finalizing stuck worker ' + i); |
| 286 | + |
| 287 | + // Force finalize the stuck suite (finalize checks for double-finalization) |
| 288 | + suite.finalize(false); // Mark as failed |
| 289 | + |
| 290 | + // Reset worker state |
| 291 | + workerLastActivity[i] = now; |
| 292 | + } |
| 293 | + } |
| 294 | + }; |
| 295 | +
|
253 | 296 | var indexFromWorkerName = function(worker) { |
254 | 297 | return Number(worker.name.substr(WORKER_NAME_PREFIX.length)); |
255 | 298 | }; |
|
276 | 319 | var i = indexFromWorkerName(worker), |
277 | 320 | testSuite = suitesInProgress[i]; |
278 | 321 |
|
| 322 | + workerLastActivity[i] = Date.now(); // Mark worker activity |
279 | 323 | notifyIsAlive(); |
280 | 324 |
|
281 | 325 | var testCase = { |
|
329 | 373 | ); |
330 | 374 | } |
331 | 375 |
|
332 | | - $.post(@Html.Raw(Json.Serialize(Url.Action("NotifyTestStarted"))), { name: getTestCaseName(testSuite, qunitData) }); |
| 376 | + $.post(@Html.Raw(Json.Serialize(Url.Action("NotifyTestStarted"))), { name: getTestCaseName(testSuite, qunitData) }) |
| 377 | + .fail(function(jqXHR, textStatus, errorThrown) { |
| 378 | + console.warn('NotifyTestStarted failed:', textStatus, errorThrown); |
| 379 | + }); |
333 | 380 | }; |
334 | 381 |
|
335 | 382 | var indicateTestStatusInTitle = function(failed) { |
|
384 | 431 | testCases, |
385 | 432 | testCase; |
386 | 433 |
|
| 434 | + workerLastActivity[i] = Date.now(); // Mark worker activity |
| 435 | + |
387 | 436 | // Always notify on test done (removed throttling to prevent stalling) |
388 | 437 | notifyDeviceTestManager("QUnit.testCaseDone"); |
389 | 438 | notifyIsAlive(); |
|
416 | 465 | ); |
417 | 466 | } |
418 | 467 |
|
419 | | - $.post(@Html.Raw(Json.Serialize(Url.Action("NotifyTestCompleted"))), { name: getTestCaseName(testSuite, qunitData), passed: qunitData.passed === qunitData.total}); |
| 468 | + $.post(@Html.Raw(Json.Serialize(Url.Action("NotifyTestCompleted"))), { name: getTestCaseName(testSuite, qunitData), passed: qunitData.passed === qunitData.total}) |
| 469 | + .fail(function(jqXHR, textStatus, errorThrown) { |
| 470 | + console.warn('NotifyTestCompleted failed:', textStatus, errorThrown); |
| 471 | + }); |
420 | 472 | }; |
421 | 473 |
|
422 | 474 | window.RUNNER_ON_DONE = function(worker, qunitData) { |
423 | | - var suite = suitesInProgress[indexFromWorkerName(worker)], |
| 475 | + var i = indexFromWorkerName(worker), |
| 476 | + suite = suitesInProgress[i], |
424 | 477 | passed = !qunitData.failed; |
425 | 478 |
|
| 479 | + console.log('RUNNER_ON_DONE: worker=' + i + ', suite=' + (suite ? suite.name : 'null') + ', busyCount=' + busyCount); |
| 480 | +
|
426 | 481 | if(suite) { |
| 482 | + // finalize() handles busyCount-- internally and prevents double-finalization |
427 | 483 | suite.finalize(passed, qunitData.total); |
428 | 484 | notifySuiteFinalized(suite.name, passed, qunitData.runtime); |
| 485 | + } else { |
| 486 | + console.warn('RUNNER_ON_DONE: suite is null for worker ' + i + ' - likely already finalized'); |
| 487 | + // Suite is null - it was already finalized (by checkStuckWorkers or previous DONE) |
| 488 | + // busyCount was already decremented in finalize(), so we just call nextUrl |
| 489 | + setTimeout(function() { nextUrl.call(this, i); }, 0); |
429 | 490 | } |
430 | 491 | }; |
431 | 492 |
|
|
459 | 520 | } |
460 | 521 |
|
461 | 522 | function notifySuiteFinalized(name, passed, runtime) { |
462 | | - $.post(@Html.Raw(Json.Serialize(Url.Action("NotifySuiteFinalized"))), { name: name, passed: passed, runtime: runtime }); |
| 523 | + $.post(@Html.Raw(Json.Serialize(Url.Action("NotifySuiteFinalized"))), { name: name, passed: passed, runtime: runtime }) |
| 524 | + .fail(function(jqXHR, textStatus, errorThrown) { |
| 525 | + console.warn('NotifySuiteFinalized failed:', textStatus, errorThrown); |
| 526 | + }); |
463 | 527 | } |
464 | 528 | function notifyIsAlive(){ |
465 | | - $.post(@Html.Raw(Json.Serialize(Url.Action("NotifyIsAlive")))); |
| 529 | + $.post(@Html.Raw(Json.Serialize(Url.Action("NotifyIsAlive")))) |
| 530 | + .fail(function(jqXHR, textStatus, errorThrown) { |
| 531 | + console.warn('NotifyIsAlive failed:', textStatus, errorThrown); |
| 532 | + }); |
466 | 533 | } |
467 | 534 |
|
| 535 | + // Check for stuck workers every 30 seconds |
| 536 | + setInterval(checkStuckWorkers, 30000); |
| 537 | +
|
468 | 538 | function roundTime(time) { |
469 | 539 | return +(time.toFixed(3)); |
470 | 540 | } |
|
0 commit comments