1 /* 2 * Copyright (C) 2011 Google Inc. All rights reserved. 3 * 4 * Redistribution and use in source and binary forms, with or without 5 * modification, are permitted provided that the following conditions 6 * are met: 7 * 1. Redistributions of source code must retain the above copyright 8 * notice, this list of conditions and the following disclaimer. 9 * 2. Redistributions in binary form must reproduce the above copyright 10 * notice, this list of conditions and the following disclaimer in the 11 * documentation and/or other materials provided with the distribution. 12 * 13 * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' 14 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, 15 * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 16 * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS 17 * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 18 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 19 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 20 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 21 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 22 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF 23 * THE POSSIBILITY OF SUCH DAMAGE. 24 */ 25 26 var controllers = controllers || {}; 27 28 (function(){ 29 30 var kCheckoutUnavailableMessage = 'Failed! Garden-o-matic needs a local server to modify your working copy. Please run "webkit-patch garden-o-matic" start the local server.'; 31 32 // FIXME: Where should this function go? 33 function rebaselineWithStatusUpdates(failureInfoList, resultsByTest) 34 { 35 var statusView = new ui.StatusArea('Rebaseline'); 36 var id = statusView.newId(); 37 38 var failuresToRebaseline = []; 39 var testNamesLogged = []; 40 failureInfoList.forEach(function(failureInfo) { 41 if (isAnyReftest(failureInfo.testName, resultsByTest)) { 42 if (testNamesLogged.indexOf(failureInfo.testName) == -1) { 43 statusView.addMessage(id, failureInfo.testName + ' is a ref test, skipping'); 44 testNamesLogged.push(failureInfo.testName); 45 } 46 } else { 47 failuresToRebaseline.push(failureInfo); 48 if (testNamesLogged.indexOf(failureInfo.testName) == -1) { 49 statusView.addMessage(id, 'Rebaselining ' + failureInfo.testName + '...'); 50 testNamesLogged.push(failureInfo.testName); 51 } 52 } 53 }); 54 55 if (failuresToRebaseline.length) { 56 checkout.rebaseline(failuresToRebaseline, function() { 57 statusView.addFinalMessage(id, 'Rebaseline done! Please land with "webkit-patch land-cowhand".'); 58 }, function(failureInfo) { 59 statusView.addMessage(id, failureInfo.testName + ' on ' + ui.displayNameForBuilder(failureInfo.builderName)); 60 }, function() { 61 statusView.addFinalMessage(id, kCheckoutUnavailableMessage); 62 }); 63 } else { 64 statusView.addFinalMessage(id, 'No non-reftests left to rebaseline!') 65 } 66 } 67 68 // FIXME: This is duplicated from ui/results.js :(. 69 function isAnyReftest(testName, resultsByTest) 70 { 71 return Object.keys(resultsByTest[testName]).map(function(builder) { 72 return resultsByTest[testName][builder]; 73 }).some(function(resultNode) { 74 return resultNode.reftest_type && resultNode.reftest_type.length; 75 }); 76 } 77 78 // FIXME: Where should this function go? 79 function updateExpectationsWithStatusUpdates(failureInfoList) 80 { 81 var statusView = new ui.StatusArea('Expectations Update'); 82 var id = statusView.newId(); 83 84 var testNames = base.uniquifyArray(failureInfoList.map(function(failureInfo) { return failureInfo.testName; })); 85 var testName = testNames.length == 1 ? testNames[0] : testNames.length + ' tests'; 86 statusView.addMessage(id, 'Updating expectations of ' + testName + '...'); 87 88 checkout.updateExpectations(failureInfoList, function() { 89 statusView.addFinalMessage(id, 'Expectations update done! Please land with "webkit-patch land-cowhand".'); 90 }, function() { 91 statusView.addFinalMessage(id, kCheckoutUnavailableMessage); 92 }); 93 } 94 95 controllers.ResultsDetails = base.extends(Object, { 96 init: function(view, resultsByTest) 97 { 98 this._view = view; 99 this._resultsByTest = resultsByTest; 100 this._view.setResultsByTest(resultsByTest); 101 102 this._view.firstResult(); 103 104 $(this._view).bind('next', this.onNext.bind(this)); 105 $(this._view).bind('previous', this.onPrevious.bind(this)); 106 $(this._view).bind('rebaseline', this.onRebaseline.bind(this)); 107 $(this._view).bind('expectfailure', this.onUpdateExpectations.bind(this)); 108 }, 109 onNext: function() 110 { 111 this._view.nextResult(); 112 }, 113 onPrevious: function() 114 { 115 this._view.previousResult(); 116 }, 117 _failureInfoList: function() 118 { 119 var testName = this._view.currentTestName(); 120 return Object.keys(this._resultsByTest[testName]).map(function(builderName) { 121 return results.failureInfoForTestAndBuilder(this._resultsByTest, testName, builderName); 122 }.bind(this)); 123 }, 124 onRebaseline: function() 125 { 126 rebaselineWithStatusUpdates(this._failureInfoList(), this._resultsByTest); 127 this._view.nextTest(); 128 }, 129 onUpdateExpectations: function() 130 { 131 updateExpectationsWithStatusUpdates(this._failureInfoList()); 132 } 133 }); 134 135 controllers.ExpectedFailures = base.extends(Object, { 136 init: function(model, view, delegate) 137 { 138 this._model = model; 139 this._view = view; 140 this._delegate = delegate; 141 }, 142 update: function() 143 { 144 var expectedFailures = results.expectedFailuresByTest(this._model.resultsByBuilder); 145 var failingTestsList = Object.keys(expectedFailures); 146 147 $(this._view).empty(); 148 base.forEachDirectory(failingTestsList, function(label, testsFailingInDirectory) { 149 var listItem = new ui.failures.ListItem(label, testsFailingInDirectory); 150 this._view.appendChild(listItem); 151 $(listItem).bind('examine', function() { 152 this.onExamine(testsFailingInDirectory); 153 }.bind(this)); 154 }.bind(this)); 155 }, 156 onExamine: function(failingTestsList) 157 { 158 var resultsView = new ui.results.View({ 159 fetchResultsURLs: results.fetchResultsURLs 160 }); 161 var failuresByTest = base.filterDictionary( 162 results.expectedFailuresByTest(this._model.resultsByBuilder), 163 function(key) { 164 return failingTestsList.indexOf(key) != -1; 165 }); 166 var controller = new controllers.ResultsDetails(resultsView, failuresByTest); 167 this._delegate.showResults(resultsView); 168 } 169 }); 170 171 var FailureStreamController = base.extends(Object, { 172 _resultsFilter: null, 173 _keyFor: function(failureAnalysis) { throw "Not implemented!"; }, 174 _createFailureView: function(failureAnalysis) { throw "Not implemented!"; }, 175 176 init: function(model, view, delegate) 177 { 178 this._model = model; 179 this._view = view; 180 this._delegate = delegate; 181 this._testFailures = new base.UpdateTracker(); 182 }, 183 update: function(failureAnalysis) 184 { 185 var key = this._keyFor(failureAnalysis); 186 var failure = this._testFailures.get(key); 187 if (!failure) { 188 failure = this._createFailureView(failureAnalysis); 189 this._view.add(failure); 190 $(failure).bind('examine', function() { 191 this.onExamine(failure); 192 }.bind(this)); 193 $(failure).bind('rebaseline', function() { 194 this.onRebaseline(failure); 195 }.bind(this)); 196 $(failure).bind('expectfailure', function() { 197 this.onUpdateExpectations(failure); 198 }.bind(this)); 199 } 200 failure.addFailureAnalysis(failureAnalysis); 201 this._testFailures.update(key, failure); 202 return failure; 203 }, 204 purge: function() { 205 this._testFailures.purge(function(failure) { 206 failure.dismiss(); 207 }); 208 this._testFailures.forEach(function(failure) { 209 failure.purge(); 210 }); 211 }, 212 onExamine: function(failures) 213 { 214 var resultsView = new ui.results.View({ 215 fetchResultsURLs: results.fetchResultsURLs 216 }); 217 218 var testNameList = failures.testNameList(); 219 var failuresByTest = base.filterDictionary( 220 this._resultsFilter(this._model.resultsByBuilder), 221 function(key) { 222 return testNameList.indexOf(key) != -1; 223 }); 224 225 var controller = new controllers.ResultsDetails(resultsView, failuresByTest); 226 this._delegate.showResults(resultsView); 227 }, 228 _toFailureInfoList: function(failures) 229 { 230 return base.flattenArray(failures.testNameList().map(model.unexpectedFailureInfoForTestName)); 231 }, 232 onRebaseline: function(failures) 233 { 234 var testNameList = failures.testNameList(); 235 var failuresByTest = base.filterDictionary( 236 this._resultsFilter(this._model.resultsByBuilder), 237 function(key) { 238 return testNameList.indexOf(key) != -1; 239 }); 240 241 rebaselineWithStatusUpdates(this._toFailureInfoList(failures), failuresByTest); 242 }, 243 onUpdateExpectations: function(failures) 244 { 245 updateExpectationsWithStatusUpdates(this._toFailureInfoList(failures)); 246 } 247 }); 248 249 controllers.UnexpectedFailures = base.extends(FailureStreamController, { 250 _resultsFilter: results.unexpectedFailuresByTest, 251 252 _impliedFirstFailingRevision: function(failureAnalysis) 253 { 254 return failureAnalysis.newestPassingRevision + 1; 255 }, 256 _keyFor: function(failureAnalysis) 257 { 258 return failureAnalysis.newestPassingRevision + "+" + failureAnalysis.oldestFailingRevision; 259 }, 260 _createFailureView: function(failureAnalysis) 261 { 262 var failure = new ui.notifications.FailingTestsSummary(); 263 model.commitDataListForRevisionRange(this._impliedFirstFailingRevision(failureAnalysis), failureAnalysis.oldestFailingRevision).forEach(function(commitData) { 264 var suspiciousCommit = failure.addCommitData(commitData); 265 $(suspiciousCommit).bind('rollout', function() { 266 this.onRollout(commitData.revision, failure.testNameList()); 267 }.bind(this)); 268 $(failure).bind('blame', function() { 269 this.onBlame(failure, commitData); 270 }.bind(this)); 271 }, this); 272 273 return failure; 274 }, 275 update: function(failureAnalysis) 276 { 277 var failure = FailureStreamController.prototype.update.call(this, failureAnalysis); 278 failure.updateBuilderResults(model.buildersInFlightForRevision(this._impliedFirstFailingRevision(failureAnalysis))); 279 }, 280 length: function() 281 { 282 return this._testFailures.length(); 283 }, 284 onBlame: function(failure, commitData) 285 { 286 failure.pinToCommitData(commitData); 287 $('.action', failure).each(function() { 288 // FIXME: This isn't the right way of finding and disabling this action. 289 if (this.textContent == 'Blame') 290 this.disabled = true; 291 }); 292 }, 293 onRollout: function(revision, testNameList) 294 { 295 checkout.rollout(revision, ui.rolloutReasonForTestNameList(testNameList), $.noop, function() { 296 // FIXME: We should have a better error UI. 297 alert(kCheckoutUnavailableMessage); 298 }); 299 } 300 }); 301 302 controllers.Failures = base.extends(FailureStreamController, { 303 _resultsFilter: results.expectedFailuresByTest, 304 305 _keyFor: function(failureAnalysis) 306 { 307 return base.dirName(failureAnalysis.testName); 308 }, 309 _createFailureView: function(failureAnalysis) 310 { 311 return new ui.notifications.FailingTests(); 312 }, 313 }); 314 315 controllers.FailingBuilders = base.extends(Object, { 316 init: function(view, message) 317 { 318 this._view = view; 319 this._message = message; 320 this._notification = null; 321 }, 322 hasFailures: function() 323 { 324 return !!this._notification; 325 }, 326 update: function(failuresList) 327 { 328 if (Object.keys(failuresList).length == 0) { 329 if (this._notification) { 330 this._notification.dismiss(); 331 this._notification = null; 332 } 333 return; 334 } 335 if (!this._notification) { 336 this._notification = new ui.notifications.BuildersFailing(this._message); 337 this._view.add(this._notification); 338 } 339 // FIXME: We should provide regression ranges for the failing builders. 340 // This doesn't seem to happen often enough to worry too much about that, however. 341 this._notification.setFailingBuilders(failuresList); 342 } 343 }); 344 345 })(); 346