blob: ccd29c205aa640edec00b6b1ea33732d5043a63f [file] [log] [blame]
epoger@google.comf9d134d2013-09-27 15:02:44 +00001/*
2 * Loader:
epoger@google.comafaad3d2013-09-30 15:06:25 +00003 * Reads GM result reports written out by results.py, and imports
4 * them into $scope.categories and $scope.testData .
epoger@google.comf9d134d2013-09-27 15:02:44 +00005 */
6var Loader = angular.module(
7 'Loader',
8 []
9);
epoger@google.com5f2bb002013-10-02 18:57:48 +000010
epoger@google.comad0e5522013-10-24 15:38:27 +000011
epoger@google.com5f2bb002013-10-02 18:57:48 +000012// TODO(epoger): Combine ALL of our filtering operations (including
13// truncation) into this one filter, so that runs most efficiently?
14// (We would have to make sure truncation still took place after
15// sorting, though.)
16Loader.filter(
17 'removeHiddenItems',
18 function() {
epoger@google.comeb832592013-10-23 15:07:26 +000019 return function(unfilteredItems, hiddenResultTypes, hiddenConfigs,
epoger@google.comf4394d52013-10-29 15:49:40 +000020 builderSubstring, testSubstring, viewingTab) {
epoger@google.com5f2bb002013-10-02 18:57:48 +000021 var filteredItems = [];
22 for (var i = 0; i < unfilteredItems.length; i++) {
epoger@google.com9fb6c8a2013-10-09 18:05:58 +000023 var item = unfilteredItems[i];
epoger@google.com055e3b52013-10-26 14:31:11 +000024 // For performance, we examine the "set" objects directly rather
25 // than calling $scope.isValueInSet().
26 // Besides, I don't think we have access to $scope in here...
epoger@google.com9fb6c8a2013-10-09 18:05:58 +000027 if (!(true == hiddenResultTypes[item.resultType]) &&
epoger@google.comeb832592013-10-23 15:07:26 +000028 !(true == hiddenConfigs[item.config]) &&
epoger@google.comf4394d52013-10-29 15:49:40 +000029 !(-1 == item.builder.indexOf(builderSubstring)) &&
30 !(-1 == item.test.indexOf(testSubstring)) &&
epoger@google.comeb832592013-10-23 15:07:26 +000031 (viewingTab == item.tab)) {
epoger@google.com9fb6c8a2013-10-09 18:05:58 +000032 filteredItems.push(item);
33 }
epoger@google.com5f2bb002013-10-02 18:57:48 +000034 }
35 return filteredItems;
36 };
37 }
38);
39
epoger@google.comad0e5522013-10-24 15:38:27 +000040
epoger@google.comf9d134d2013-09-27 15:02:44 +000041Loader.controller(
42 'Loader.Controller',
epoger@google.com2682c902013-12-05 16:05:16 +000043 function($scope, $http, $filter, $location, $timeout) {
epoger@google.com542b65f2013-10-15 20:10:33 +000044 $scope.windowTitle = "Loading GM Results...";
epoger@google.comdcb4e652013-10-11 18:45:33 +000045 var resultsToLoad = $location.search().resultsToLoad;
46 $scope.loadingMessage = "Loading results of type '" + resultsToLoad +
47 "', please wait...";
48
epoger@google.comad0e5522013-10-24 15:38:27 +000049 /**
50 * On initial page load, load a full dictionary of results.
51 * Once the dictionary is loaded, unhide the page elements so they can
52 * render the data.
53 */
epoger@google.comdcb4e652013-10-11 18:45:33 +000054 $http.get("/results/" + resultsToLoad).success(
55 function(data, status, header, config) {
epoger@google.com2682c902013-12-05 16:05:16 +000056 if (data.header.resultsStillLoading) {
57 $scope.loadingMessage =
58 "Server is still loading initial results; will retry at " +
59 $scope.localTimeString(data.header.timeNextUpdateAvailable);
60 $timeout(
61 function(){location.reload();},
62 (data.header.timeNextUpdateAvailable * 1000) - new Date().getTime());
63 } else {
64 $scope.loadingMessage = "Processing data, please wait...";
epoger@google.comdcb4e652013-10-11 18:45:33 +000065
epoger@google.com2682c902013-12-05 16:05:16 +000066 $scope.header = data.header;
67 $scope.categories = data.categories;
68 $scope.testData = data.testData;
69 $scope.sortColumn = 'weightedDiffMeasure';
70 $scope.showTodos = false;
epoger@google.com5f2bb002013-10-02 18:57:48 +000071
epoger@google.com2682c902013-12-05 16:05:16 +000072 $scope.showSubmitAdvancedSettings = false;
73 $scope.submitAdvancedSettings = {};
74 $scope.submitAdvancedSettings['reviewed-by-human'] = true;
75 $scope.submitAdvancedSettings['ignore-failure'] = false;
76 $scope.submitAdvancedSettings['bug'] = '';
epoger@google.com055e3b52013-10-26 14:31:11 +000077
epoger@google.com2682c902013-12-05 16:05:16 +000078 // Create the list of tabs (lists into which the user can file each
79 // test). This may vary, depending on isEditable.
80 $scope.tabs = [
81 'Unfiled', 'Hidden'
82 ];
83 if (data.header.isEditable) {
84 $scope.tabs = $scope.tabs.concat(
85 ['Pending Approval']);
86 }
87 $scope.defaultTab = $scope.tabs[0];
88 $scope.viewingTab = $scope.defaultTab;
89
90 // Track the number of results on each tab.
91 $scope.numResultsPerTab = {};
92 for (var i = 0; i < $scope.tabs.length; i++) {
93 $scope.numResultsPerTab[$scope.tabs[i]] = 0;
94 }
95 $scope.numResultsPerTab[$scope.defaultTab] = $scope.testData.length;
96
97 // Add index and tab fields to all records.
98 for (var i = 0; i < $scope.testData.length; i++) {
99 $scope.testData[i].index = i;
100 $scope.testData[i].tab = $scope.defaultTab;
101 }
102
103 // Arrays within which the user can toggle individual elements.
104 $scope.selectedItems = [];
105
106 // Sets within which the user can toggle individual elements.
107 $scope.hiddenResultTypes = {
108 'failure-ignored': true,
109 'no-comparison': true,
110 'succeeded': true,
111 };
112 $scope.allResultTypes = Object.keys(data.categories['resultType']);
113 $scope.hiddenConfigs = {};
114 $scope.allConfigs = Object.keys(data.categories['config']);
115
116 // Associative array of partial string matches per category.
117 $scope.categoryValueMatch = {};
118 $scope.categoryValueMatch.builder = "";
119 $scope.categoryValueMatch.test = "";
120
121 $scope.updateResults();
122 $scope.loadingMessage = "";
123 $scope.windowTitle = "Current GM Results";
epoger@google.comeb832592013-10-23 15:07:26 +0000124 }
epoger@google.comdcb4e652013-10-11 18:45:33 +0000125 }
126 ).error(
127 function(data, status, header, config) {
128 $scope.loadingMessage = "Failed to load results of type '"
129 + resultsToLoad + "'";
epoger@google.com542b65f2013-10-15 20:10:33 +0000130 $scope.windowTitle = "Failed to Load GM Results";
epoger@google.comf9d134d2013-09-27 15:02:44 +0000131 }
132 );
epoger@google.com5f2bb002013-10-02 18:57:48 +0000133
epoger@google.com9fb6c8a2013-10-09 18:05:58 +0000134
epoger@google.comad0e5522013-10-24 15:38:27 +0000135 //
epoger@google.com055e3b52013-10-26 14:31:11 +0000136 // Select/Clear/Toggle all tests.
137 //
138
139 /**
140 * Select all currently showing tests.
141 */
142 $scope.selectAllItems = function() {
143 var numItemsShowing = $scope.limitedTestData.length;
144 for (var i = 0; i < numItemsShowing; i++) {
145 var index = $scope.limitedTestData[i].index;
146 if (!$scope.isValueInArray(index, $scope.selectedItems)) {
147 $scope.toggleValueInArray(index, $scope.selectedItems);
148 }
149 }
150 }
151
152 /**
153 * Deselect all currently showing tests.
154 */
155 $scope.clearAllItems = function() {
156 var numItemsShowing = $scope.limitedTestData.length;
157 for (var i = 0; i < numItemsShowing; i++) {
158 var index = $scope.limitedTestData[i].index;
159 if ($scope.isValueInArray(index, $scope.selectedItems)) {
160 $scope.toggleValueInArray(index, $scope.selectedItems);
161 }
162 }
163 }
164
165 /**
166 * Toggle selection of all currently showing tests.
167 */
168 $scope.toggleAllItems = function() {
169 var numItemsShowing = $scope.limitedTestData.length;
170 for (var i = 0; i < numItemsShowing; i++) {
171 var index = $scope.limitedTestData[i].index;
172 $scope.toggleValueInArray(index, $scope.selectedItems);
173 }
174 }
175
176
177 //
epoger@google.comad0e5522013-10-24 15:38:27 +0000178 // Tab operations.
179 //
epoger@google.com5f2bb002013-10-02 18:57:48 +0000180
epoger@google.comad0e5522013-10-24 15:38:27 +0000181 /**
182 * Change the selected tab.
183 *
184 * @param tab (string): name of the tab to select
185 */
epoger@google.comeb832592013-10-23 15:07:26 +0000186 $scope.setViewingTab = function(tab) {
187 $scope.viewingTab = tab;
188 $scope.updateResults();
189 }
190
epoger@google.comeb832592013-10-23 15:07:26 +0000191 /**
192 * Move the items in $scope.selectedItems to a different tab,
193 * and then clear $scope.selectedItems.
194 *
195 * @param newTab (string): name of the tab to move the tests to
196 */
197 $scope.moveSelectedItemsToTab = function(newTab) {
198 $scope.moveItemsToTab($scope.selectedItems, newTab);
199 $scope.selectedItems = [];
200 $scope.updateResults();
201 }
202
203 /**
204 * Move a subset of $scope.testData to a different tab.
205 *
206 * @param itemIndices (array of ints): indices into $scope.testData
207 * indicating which test results to move
208 * @param newTab (string): name of the tab to move the tests to
209 */
210 $scope.moveItemsToTab = function(itemIndices, newTab) {
211 var itemIndex;
212 var numItems = itemIndices.length;
213 for (var i = 0; i < numItems; i++) {
214 itemIndex = itemIndices[i];
215 $scope.numResultsPerTab[$scope.testData[itemIndex].tab]--;
216 $scope.testData[itemIndex].tab = newTab;
217 }
218 $scope.numResultsPerTab[newTab] += numItems;
219 }
220
epoger@google.comad0e5522013-10-24 15:38:27 +0000221
222 //
223 // updateResults() and friends.
224 //
225
226 /**
227 * Set $scope.areUpdatesPending (to enable/disable the Update Results
228 * button).
229 *
230 * TODO(epoger): We could reduce the amount of code by just setting the
231 * variable directly (from, e.g., a button's ng-click handler). But when
232 * I tried that, the HTML elements depending on the variable did not get
233 * updated.
234 * It turns out that this is due to variable scoping within an ng-repeat
235 * element; see http://stackoverflow.com/questions/15388344/behavior-of-assignment-expression-invoked-by-ng-click-within-ng-repeat
236 *
237 * @param val boolean value to set $scope.areUpdatesPending to
238 */
239 $scope.setUpdatesPending = function(val) {
240 $scope.areUpdatesPending = val;
241 }
242
243 /**
244 * Update the displayed results, based on filters/settings.
245 */
epoger@google.com5f2bb002013-10-02 18:57:48 +0000246 $scope.updateResults = function() {
247 $scope.displayLimit = $scope.displayLimitPending;
248 // TODO(epoger): Every time we apply a filter, AngularJS creates
249 // another copy of the array. Is there a way we can filter out
250 // the items as they are displayed, rather than storing multiple
251 // array copies? (For better performance.)
epoger@google.comeb832592013-10-23 15:07:26 +0000252
253 if ($scope.viewingTab == $scope.defaultTab) {
epoger@google.com9dddf6f2013-11-08 16:25:25 +0000254
255 // TODO(epoger): Until we allow the user to reverse sort order,
256 // there are certain columns we want to sort in a different order.
257 var doReverse = (
258 ($scope.sortColumn == 'percentDifferingPixels') ||
259 ($scope.sortColumn == 'weightedDiffMeasure'));
260
epoger@google.comeb832592013-10-23 15:07:26 +0000261 $scope.filteredTestData =
262 $filter("orderBy")(
263 $filter("removeHiddenItems")(
264 $scope.testData,
265 $scope.hiddenResultTypes,
266 $scope.hiddenConfigs,
epoger@google.comf4394d52013-10-29 15:49:40 +0000267 $scope.categoryValueMatch.builder,
268 $scope.categoryValueMatch.test,
epoger@google.comeb832592013-10-23 15:07:26 +0000269 $scope.viewingTab
270 ),
epoger@google.com9dddf6f2013-11-08 16:25:25 +0000271 $scope.sortColumn, doReverse);
epoger@google.comeb832592013-10-23 15:07:26 +0000272 $scope.limitedTestData = $filter("limitTo")(
273 $scope.filteredTestData, $scope.displayLimit);
274 } else {
275 $scope.filteredTestData =
276 $filter("orderBy")(
277 $filter("filter")(
278 $scope.testData,
279 {tab: $scope.viewingTab},
280 true
281 ),
282 $scope.sortColumn);
epoger@google.com055e3b52013-10-26 14:31:11 +0000283 $scope.limitedTestData = $scope.filteredTestData;
epoger@google.comeb832592013-10-23 15:07:26 +0000284 }
epoger@google.com5f2bb002013-10-02 18:57:48 +0000285 $scope.imageSize = $scope.imageSizePending;
epoger@google.comad0e5522013-10-24 15:38:27 +0000286 $scope.setUpdatesPending(false);
epoger@google.com5f2bb002013-10-02 18:57:48 +0000287 }
288
epoger@google.comad0e5522013-10-24 15:38:27 +0000289 /**
290 * Re-sort the displayed results.
291 *
292 * @param sortColumn (string): name of the column to sort on
293 */
epoger@google.com5f2bb002013-10-02 18:57:48 +0000294 $scope.sortResultsBy = function(sortColumn) {
295 $scope.sortColumn = sortColumn;
296 $scope.updateResults();
297 }
epoger@google.comeb832592013-10-23 15:07:26 +0000298
epoger@google.comf4394d52013-10-29 15:49:40 +0000299 /**
300 * Set $scope.categoryValueMatch[name] = value, and update results.
301 *
302 * @param name
303 * @param value
304 */
305 $scope.setCategoryValueMatch = function(name, value) {
306 $scope.categoryValueMatch[name] = value;
307 $scope.updateResults();
308 }
309
310 /**
311 * Update $scope.hiddenResultTypes so that ONLY this resultType is showing,
312 * and update the visible results.
313 *
314 * @param resultType
315 */
316 $scope.showOnlyResultType = function(resultType) {
317 $scope.hiddenResultTypes = {};
318 // TODO(epoger): Maybe change $scope.allResultTypes to be a Set like
319 // $scope.hiddenResultTypes (rather than an array), so this operation is
320 // simpler (just assign or add allResultTypes to hiddenResultTypes).
321 $scope.toggleValuesInSet($scope.allResultTypes, $scope.hiddenResultTypes);
322 $scope.toggleValueInSet(resultType, $scope.hiddenResultTypes);
323 $scope.updateResults();
324 }
325
326 /**
327 * Update $scope.hiddenConfigs so that ONLY this config is showing,
328 * and update the visible results.
329 *
330 * @param config
331 */
332 $scope.showOnlyConfig = function(config) {
333 $scope.hiddenConfigs = {};
334 $scope.toggleValuesInSet($scope.allConfigs, $scope.hiddenConfigs);
335 $scope.toggleValueInSet(config, $scope.hiddenConfigs);
336 $scope.updateResults();
337 }
338
epoger@google.comad0e5522013-10-24 15:38:27 +0000339
340 //
341 // Operations for sending info back to the server.
342 //
343
epoger@google.comeb832592013-10-23 15:07:26 +0000344 /**
345 * Tell the server that the actual results of these particular tests
346 * are acceptable.
347 *
348 * @param testDataSubset an array of test results, most likely a subset of
349 * $scope.testData (perhaps with some modifications)
350 */
351 $scope.submitApprovals = function(testDataSubset) {
352 $scope.submitPending = true;
epoger@google.com055e3b52013-10-26 14:31:11 +0000353
354 // Convert bug text field to null or 1-item array.
355 var bugs = null;
356 var bugNumber = parseInt($scope.submitAdvancedSettings['bug']);
357 if (!isNaN(bugNumber)) {
358 bugs = [bugNumber];
359 }
360
361 // TODO(epoger): This is a suboptimal way to prevent users from
362 // rebaselining failures in alternative renderModes, but it does work.
363 // For a better solution, see
364 // https://code.google.com/p/skia/issues/detail?id=1748 ('gm: add new
365 // result type, RenderModeMismatch')
366 var encounteredComparisonConfig = false;
367
epoger@google.comeb832592013-10-23 15:07:26 +0000368 var newResults = [];
369 for (var i = 0; i < testDataSubset.length; i++) {
370 var actualResult = testDataSubset[i];
371 var expectedResult = {
372 builder: actualResult['builder'],
373 test: actualResult['test'],
374 config: actualResult['config'],
375 expectedHashType: actualResult['actualHashType'],
376 expectedHashDigest: actualResult['actualHashDigest'],
377 };
epoger@google.com055e3b52013-10-26 14:31:11 +0000378 if (0 == expectedResult.config.indexOf('comparison-')) {
379 encounteredComparisonConfig = true;
380 }
381
382 // Advanced settings...
383 expectedResult['reviewed-by-human'] =
384 $scope.submitAdvancedSettings['reviewed-by-human'];
385 if (true == $scope.submitAdvancedSettings['ignore-failure']) {
386 // if it's false, don't send it at all (just keep the default)
epoger@google.com1e698af2013-11-05 21:00:24 +0000387 expectedResult['ignore-failure'] = true;
epoger@google.com055e3b52013-10-26 14:31:11 +0000388 }
389 expectedResult['bugs'] = bugs;
390
epoger@google.comeb832592013-10-23 15:07:26 +0000391 newResults.push(expectedResult);
392 }
epoger@google.com055e3b52013-10-26 14:31:11 +0000393 if (encounteredComparisonConfig) {
394 alert("Approval failed -- you cannot approve results with config " +
395 "type comparison-*");
396 $scope.submitPending = false;
397 return;
398 }
epoger@google.comeb832592013-10-23 15:07:26 +0000399 $http({
400 method: "POST",
401 url: "/edits",
402 data: {
403 oldResultsType: $scope.header.type,
404 oldResultsHash: $scope.header.dataHash,
405 modifications: newResults
406 }
407 }).success(function(data, status, headers, config) {
408 var itemIndicesToMove = [];
409 for (var i = 0; i < testDataSubset.length; i++) {
410 itemIndicesToMove.push(testDataSubset[i].index);
411 }
412 $scope.moveItemsToTab(itemIndicesToMove,
413 "HackToMakeSureThisItemDisappears");
414 $scope.updateResults();
415 alert("New baselines submitted successfully!\n\n" +
416 "You still need to commit the updated expectations files on " +
417 "the server side to the Skia repo.\n\n" +
418 "Also: in order to see the complete updated data, or to submit " +
419 "more baselines, you will need to reload your client.");
420 $scope.submitPending = false;
421 }).error(function(data, status, headers, config) {
422 alert("There was an error submitting your baselines.\n\n" +
423 "Please see server-side log for details.");
424 $scope.submitPending = false;
425 });
426 }
epoger@google.comad0e5522013-10-24 15:38:27 +0000427
428
429 //
430 // Operations we use to mimic Set semantics, in such a way that
431 // checking for presence within the Set is as fast as possible.
432 // But getting a list of all values within the Set is not necessarily
433 // possible.
434 // TODO(epoger): move into a separate .js file?
435 //
436
437 /**
438 * Returns true if value "value" is present within set "set".
439 *
440 * @param value a value of any type
441 * @param set an Object which we use to mimic set semantics
442 * (this should make isValueInSet faster than if we used an Array)
443 */
444 $scope.isValueInSet = function(value, set) {
445 return (true == set[value]);
446 }
447
448 /**
449 * If value "value" is already in set "set", remove it; otherwise, add it.
450 *
451 * @param value a value of any type
452 * @param set an Object which we use to mimic set semantics
453 */
454 $scope.toggleValueInSet = function(value, set) {
455 if (true == set[value]) {
456 delete set[value];
457 } else {
458 set[value] = true;
459 }
460 }
461
epoger@google.comf4394d52013-10-29 15:49:40 +0000462 /**
463 * For each value in valueArray, call toggleValueInSet(value, set).
464 *
465 * @param valueArray
466 * @param set
467 */
468 $scope.toggleValuesInSet = function(valueArray, set) {
469 var arrayLength = valueArray.length;
470 for (var i = 0; i < arrayLength; i++) {
471 $scope.toggleValueInSet(valueArray[i], set);
472 }
473 }
474
epoger@google.comad0e5522013-10-24 15:38:27 +0000475
476 //
477 // Array operations; similar to our Set operations, but operate on a
478 // Javascript Array so we *can* easily get a list of all values in the Set.
479 // TODO(epoger): move into a separate .js file?
480 //
481
482 /**
483 * Returns true if value "value" is present within array "array".
484 *
485 * @param value a value of any type
486 * @param array a Javascript Array
487 */
488 $scope.isValueInArray = function(value, array) {
489 return (-1 != array.indexOf(value));
490 }
491
492 /**
493 * If value "value" is already in array "array", remove it; otherwise,
494 * add it.
495 *
496 * @param value a value of any type
497 * @param array a Javascript Array
498 */
499 $scope.toggleValueInArray = function(value, array) {
500 var i = array.indexOf(value);
501 if (-1 == i) {
502 array.push(value);
503 } else {
504 array.splice(i, 1);
505 }
506 }
507
508
509 //
510 // Miscellaneous utility functions.
511 // TODO(epoger): move into a separate .js file?
512 //
513
514 /**
515 * Returns a human-readable (in local time zone) time string for a
516 * particular moment in time.
517 *
518 * @param secondsPastEpoch (numeric): seconds past epoch in UTC
519 */
520 $scope.localTimeString = function(secondsPastEpoch) {
521 var d = new Date(secondsPastEpoch * 1000);
522 return d.toString();
523 }
524
epoger@google.comf9d134d2013-09-27 15:02:44 +0000525 }
526);