blob: 3b16e2e439f4d8c948936abdb50fc507ff1c5fde [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.com542b65f2013-10-15 20:10:33 +000043 function($scope, $http, $filter, $location) {
44 $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) {
56 $scope.loadingMessage = "Processing data, please wait...";
57
58 $scope.header = data.header;
59 $scope.categories = data.categories;
60 $scope.testData = data.testData;
epoger@google.com9dddf6f2013-11-08 16:25:25 +000061 $scope.sortColumn = 'weightedDiffMeasure';
epoger@google.comeb832592013-10-23 15:07:26 +000062 $scope.showTodos = false;
epoger@google.com5f2bb002013-10-02 18:57:48 +000063
epoger@google.com055e3b52013-10-26 14:31:11 +000064 $scope.showSubmitAdvancedSettings = false;
65 $scope.submitAdvancedSettings = {};
66 $scope.submitAdvancedSettings['reviewed-by-human'] = true;
epoger@google.com1e698af2013-11-05 21:00:24 +000067 $scope.submitAdvancedSettings['ignore-failure'] = false;
epoger@google.com055e3b52013-10-26 14:31:11 +000068 $scope.submitAdvancedSettings['bug'] = '';
69
epoger@google.comeb832592013-10-23 15:07:26 +000070 // Create the list of tabs (lists into which the user can file each
71 // test). This may vary, depending on isEditable.
72 $scope.tabs = [
73 'Unfiled', 'Hidden'
74 ];
75 if (data.header.isEditable) {
76 $scope.tabs = $scope.tabs.concat(
77 ['Pending Approval']);
78 }
79 $scope.defaultTab = $scope.tabs[0];
80 $scope.viewingTab = $scope.defaultTab;
81
82 // Track the number of results on each tab.
83 $scope.numResultsPerTab = {};
84 for (var i = 0; i < $scope.tabs.length; i++) {
85 $scope.numResultsPerTab[$scope.tabs[i]] = 0;
86 }
87 $scope.numResultsPerTab[$scope.defaultTab] = $scope.testData.length;
88
89 // Add index and tab fields to all records.
epoger@google.comdcb4e652013-10-11 18:45:33 +000090 for (var i = 0; i < $scope.testData.length; i++) {
91 $scope.testData[i].index = i;
epoger@google.comeb832592013-10-23 15:07:26 +000092 $scope.testData[i].tab = $scope.defaultTab;
epoger@google.comdcb4e652013-10-11 18:45:33 +000093 }
94
epoger@google.com055e3b52013-10-26 14:31:11 +000095 // Arrays within which the user can toggle individual elements.
epoger@google.comad0e5522013-10-24 15:38:27 +000096 $scope.selectedItems = [];
97
epoger@google.com055e3b52013-10-26 14:31:11 +000098 // Sets within which the user can toggle individual elements.
epoger@google.com9fb6c8a2013-10-09 18:05:58 +000099 $scope.hiddenResultTypes = {
100 'failure-ignored': true,
101 'no-comparison': true,
102 'succeeded': true,
103 };
epoger@google.comf4394d52013-10-29 15:49:40 +0000104 $scope.allResultTypes = Object.keys(data.categories['resultType']);
epoger@google.com9fb6c8a2013-10-09 18:05:58 +0000105 $scope.hiddenConfigs = {};
epoger@google.comf4394d52013-10-29 15:49:40 +0000106 $scope.allConfigs = Object.keys(data.categories['config']);
107
108 // Associative array of partial string matches per category.
109 $scope.categoryValueMatch = {};
110 $scope.categoryValueMatch.builder = "";
111 $scope.categoryValueMatch.test = "";
epoger@google.com5f2bb002013-10-02 18:57:48 +0000112
113 $scope.updateResults();
epoger@google.comdcb4e652013-10-11 18:45:33 +0000114 $scope.loadingMessage = "";
epoger@google.com542b65f2013-10-15 20:10:33 +0000115 $scope.windowTitle = "Current GM Results";
epoger@google.comdcb4e652013-10-11 18:45:33 +0000116 }
117 ).error(
118 function(data, status, header, config) {
119 $scope.loadingMessage = "Failed to load results of type '"
120 + resultsToLoad + "'";
epoger@google.com542b65f2013-10-15 20:10:33 +0000121 $scope.windowTitle = "Failed to Load GM Results";
epoger@google.comf9d134d2013-09-27 15:02:44 +0000122 }
123 );
epoger@google.com5f2bb002013-10-02 18:57:48 +0000124
epoger@google.com9fb6c8a2013-10-09 18:05:58 +0000125
epoger@google.comad0e5522013-10-24 15:38:27 +0000126 //
epoger@google.com055e3b52013-10-26 14:31:11 +0000127 // Select/Clear/Toggle all tests.
128 //
129
130 /**
131 * Select all currently showing tests.
132 */
133 $scope.selectAllItems = function() {
134 var numItemsShowing = $scope.limitedTestData.length;
135 for (var i = 0; i < numItemsShowing; i++) {
136 var index = $scope.limitedTestData[i].index;
137 if (!$scope.isValueInArray(index, $scope.selectedItems)) {
138 $scope.toggleValueInArray(index, $scope.selectedItems);
139 }
140 }
141 }
142
143 /**
144 * Deselect all currently showing tests.
145 */
146 $scope.clearAllItems = function() {
147 var numItemsShowing = $scope.limitedTestData.length;
148 for (var i = 0; i < numItemsShowing; i++) {
149 var index = $scope.limitedTestData[i].index;
150 if ($scope.isValueInArray(index, $scope.selectedItems)) {
151 $scope.toggleValueInArray(index, $scope.selectedItems);
152 }
153 }
154 }
155
156 /**
157 * Toggle selection of all currently showing tests.
158 */
159 $scope.toggleAllItems = function() {
160 var numItemsShowing = $scope.limitedTestData.length;
161 for (var i = 0; i < numItemsShowing; i++) {
162 var index = $scope.limitedTestData[i].index;
163 $scope.toggleValueInArray(index, $scope.selectedItems);
164 }
165 }
166
167
168 //
epoger@google.comad0e5522013-10-24 15:38:27 +0000169 // Tab operations.
170 //
epoger@google.com5f2bb002013-10-02 18:57:48 +0000171
epoger@google.comad0e5522013-10-24 15:38:27 +0000172 /**
173 * Change the selected tab.
174 *
175 * @param tab (string): name of the tab to select
176 */
epoger@google.comeb832592013-10-23 15:07:26 +0000177 $scope.setViewingTab = function(tab) {
178 $scope.viewingTab = tab;
179 $scope.updateResults();
180 }
181
epoger@google.comeb832592013-10-23 15:07:26 +0000182 /**
183 * Move the items in $scope.selectedItems to a different tab,
184 * and then clear $scope.selectedItems.
185 *
186 * @param newTab (string): name of the tab to move the tests to
187 */
188 $scope.moveSelectedItemsToTab = function(newTab) {
189 $scope.moveItemsToTab($scope.selectedItems, newTab);
190 $scope.selectedItems = [];
191 $scope.updateResults();
192 }
193
194 /**
195 * Move a subset of $scope.testData to a different tab.
196 *
197 * @param itemIndices (array of ints): indices into $scope.testData
198 * indicating which test results to move
199 * @param newTab (string): name of the tab to move the tests to
200 */
201 $scope.moveItemsToTab = function(itemIndices, newTab) {
202 var itemIndex;
203 var numItems = itemIndices.length;
204 for (var i = 0; i < numItems; i++) {
205 itemIndex = itemIndices[i];
206 $scope.numResultsPerTab[$scope.testData[itemIndex].tab]--;
207 $scope.testData[itemIndex].tab = newTab;
208 }
209 $scope.numResultsPerTab[newTab] += numItems;
210 }
211
epoger@google.comad0e5522013-10-24 15:38:27 +0000212
213 //
214 // updateResults() and friends.
215 //
216
217 /**
218 * Set $scope.areUpdatesPending (to enable/disable the Update Results
219 * button).
220 *
221 * TODO(epoger): We could reduce the amount of code by just setting the
222 * variable directly (from, e.g., a button's ng-click handler). But when
223 * I tried that, the HTML elements depending on the variable did not get
224 * updated.
225 * It turns out that this is due to variable scoping within an ng-repeat
226 * element; see http://stackoverflow.com/questions/15388344/behavior-of-assignment-expression-invoked-by-ng-click-within-ng-repeat
227 *
228 * @param val boolean value to set $scope.areUpdatesPending to
229 */
230 $scope.setUpdatesPending = function(val) {
231 $scope.areUpdatesPending = val;
232 }
233
234 /**
235 * Update the displayed results, based on filters/settings.
236 */
epoger@google.com5f2bb002013-10-02 18:57:48 +0000237 $scope.updateResults = function() {
238 $scope.displayLimit = $scope.displayLimitPending;
239 // TODO(epoger): Every time we apply a filter, AngularJS creates
240 // another copy of the array. Is there a way we can filter out
241 // the items as they are displayed, rather than storing multiple
242 // array copies? (For better performance.)
epoger@google.comeb832592013-10-23 15:07:26 +0000243
244 if ($scope.viewingTab == $scope.defaultTab) {
epoger@google.com9dddf6f2013-11-08 16:25:25 +0000245
246 // TODO(epoger): Until we allow the user to reverse sort order,
247 // there are certain columns we want to sort in a different order.
248 var doReverse = (
249 ($scope.sortColumn == 'percentDifferingPixels') ||
250 ($scope.sortColumn == 'weightedDiffMeasure'));
251
epoger@google.comeb832592013-10-23 15:07:26 +0000252 $scope.filteredTestData =
253 $filter("orderBy")(
254 $filter("removeHiddenItems")(
255 $scope.testData,
256 $scope.hiddenResultTypes,
257 $scope.hiddenConfigs,
epoger@google.comf4394d52013-10-29 15:49:40 +0000258 $scope.categoryValueMatch.builder,
259 $scope.categoryValueMatch.test,
epoger@google.comeb832592013-10-23 15:07:26 +0000260 $scope.viewingTab
261 ),
epoger@google.com9dddf6f2013-11-08 16:25:25 +0000262 $scope.sortColumn, doReverse);
epoger@google.comeb832592013-10-23 15:07:26 +0000263 $scope.limitedTestData = $filter("limitTo")(
264 $scope.filteredTestData, $scope.displayLimit);
265 } else {
266 $scope.filteredTestData =
267 $filter("orderBy")(
268 $filter("filter")(
269 $scope.testData,
270 {tab: $scope.viewingTab},
271 true
272 ),
273 $scope.sortColumn);
epoger@google.com055e3b52013-10-26 14:31:11 +0000274 $scope.limitedTestData = $scope.filteredTestData;
epoger@google.comeb832592013-10-23 15:07:26 +0000275 }
epoger@google.com5f2bb002013-10-02 18:57:48 +0000276 $scope.imageSize = $scope.imageSizePending;
epoger@google.comad0e5522013-10-24 15:38:27 +0000277 $scope.setUpdatesPending(false);
epoger@google.com5f2bb002013-10-02 18:57:48 +0000278 }
279
epoger@google.comad0e5522013-10-24 15:38:27 +0000280 /**
281 * Re-sort the displayed results.
282 *
283 * @param sortColumn (string): name of the column to sort on
284 */
epoger@google.com5f2bb002013-10-02 18:57:48 +0000285 $scope.sortResultsBy = function(sortColumn) {
286 $scope.sortColumn = sortColumn;
287 $scope.updateResults();
288 }
epoger@google.comeb832592013-10-23 15:07:26 +0000289
epoger@google.comf4394d52013-10-29 15:49:40 +0000290 /**
291 * Set $scope.categoryValueMatch[name] = value, and update results.
292 *
293 * @param name
294 * @param value
295 */
296 $scope.setCategoryValueMatch = function(name, value) {
297 $scope.categoryValueMatch[name] = value;
298 $scope.updateResults();
299 }
300
301 /**
302 * Update $scope.hiddenResultTypes so that ONLY this resultType is showing,
303 * and update the visible results.
304 *
305 * @param resultType
306 */
307 $scope.showOnlyResultType = function(resultType) {
308 $scope.hiddenResultTypes = {};
309 // TODO(epoger): Maybe change $scope.allResultTypes to be a Set like
310 // $scope.hiddenResultTypes (rather than an array), so this operation is
311 // simpler (just assign or add allResultTypes to hiddenResultTypes).
312 $scope.toggleValuesInSet($scope.allResultTypes, $scope.hiddenResultTypes);
313 $scope.toggleValueInSet(resultType, $scope.hiddenResultTypes);
314 $scope.updateResults();
315 }
316
317 /**
318 * Update $scope.hiddenConfigs so that ONLY this config is showing,
319 * and update the visible results.
320 *
321 * @param config
322 */
323 $scope.showOnlyConfig = function(config) {
324 $scope.hiddenConfigs = {};
325 $scope.toggleValuesInSet($scope.allConfigs, $scope.hiddenConfigs);
326 $scope.toggleValueInSet(config, $scope.hiddenConfigs);
327 $scope.updateResults();
328 }
329
epoger@google.comad0e5522013-10-24 15:38:27 +0000330
331 //
332 // Operations for sending info back to the server.
333 //
334
epoger@google.comeb832592013-10-23 15:07:26 +0000335 /**
336 * Tell the server that the actual results of these particular tests
337 * are acceptable.
338 *
339 * @param testDataSubset an array of test results, most likely a subset of
340 * $scope.testData (perhaps with some modifications)
341 */
342 $scope.submitApprovals = function(testDataSubset) {
343 $scope.submitPending = true;
epoger@google.com055e3b52013-10-26 14:31:11 +0000344
345 // Convert bug text field to null or 1-item array.
346 var bugs = null;
347 var bugNumber = parseInt($scope.submitAdvancedSettings['bug']);
348 if (!isNaN(bugNumber)) {
349 bugs = [bugNumber];
350 }
351
352 // TODO(epoger): This is a suboptimal way to prevent users from
353 // rebaselining failures in alternative renderModes, but it does work.
354 // For a better solution, see
355 // https://code.google.com/p/skia/issues/detail?id=1748 ('gm: add new
356 // result type, RenderModeMismatch')
357 var encounteredComparisonConfig = false;
358
epoger@google.comeb832592013-10-23 15:07:26 +0000359 var newResults = [];
360 for (var i = 0; i < testDataSubset.length; i++) {
361 var actualResult = testDataSubset[i];
362 var expectedResult = {
363 builder: actualResult['builder'],
364 test: actualResult['test'],
365 config: actualResult['config'],
366 expectedHashType: actualResult['actualHashType'],
367 expectedHashDigest: actualResult['actualHashDigest'],
368 };
epoger@google.com055e3b52013-10-26 14:31:11 +0000369 if (0 == expectedResult.config.indexOf('comparison-')) {
370 encounteredComparisonConfig = true;
371 }
372
373 // Advanced settings...
374 expectedResult['reviewed-by-human'] =
375 $scope.submitAdvancedSettings['reviewed-by-human'];
376 if (true == $scope.submitAdvancedSettings['ignore-failure']) {
377 // if it's false, don't send it at all (just keep the default)
epoger@google.com1e698af2013-11-05 21:00:24 +0000378 expectedResult['ignore-failure'] = true;
epoger@google.com055e3b52013-10-26 14:31:11 +0000379 }
380 expectedResult['bugs'] = bugs;
381
epoger@google.comeb832592013-10-23 15:07:26 +0000382 newResults.push(expectedResult);
383 }
epoger@google.com055e3b52013-10-26 14:31:11 +0000384 if (encounteredComparisonConfig) {
385 alert("Approval failed -- you cannot approve results with config " +
386 "type comparison-*");
387 $scope.submitPending = false;
388 return;
389 }
epoger@google.comeb832592013-10-23 15:07:26 +0000390 $http({
391 method: "POST",
392 url: "/edits",
393 data: {
394 oldResultsType: $scope.header.type,
395 oldResultsHash: $scope.header.dataHash,
396 modifications: newResults
397 }
398 }).success(function(data, status, headers, config) {
399 var itemIndicesToMove = [];
400 for (var i = 0; i < testDataSubset.length; i++) {
401 itemIndicesToMove.push(testDataSubset[i].index);
402 }
403 $scope.moveItemsToTab(itemIndicesToMove,
404 "HackToMakeSureThisItemDisappears");
405 $scope.updateResults();
406 alert("New baselines submitted successfully!\n\n" +
407 "You still need to commit the updated expectations files on " +
408 "the server side to the Skia repo.\n\n" +
409 "Also: in order to see the complete updated data, or to submit " +
410 "more baselines, you will need to reload your client.");
411 $scope.submitPending = false;
412 }).error(function(data, status, headers, config) {
413 alert("There was an error submitting your baselines.\n\n" +
414 "Please see server-side log for details.");
415 $scope.submitPending = false;
416 });
417 }
epoger@google.comad0e5522013-10-24 15:38:27 +0000418
419
420 //
421 // Operations we use to mimic Set semantics, in such a way that
422 // checking for presence within the Set is as fast as possible.
423 // But getting a list of all values within the Set is not necessarily
424 // possible.
425 // TODO(epoger): move into a separate .js file?
426 //
427
428 /**
429 * Returns true if value "value" is present within set "set".
430 *
431 * @param value a value of any type
432 * @param set an Object which we use to mimic set semantics
433 * (this should make isValueInSet faster than if we used an Array)
434 */
435 $scope.isValueInSet = function(value, set) {
436 return (true == set[value]);
437 }
438
439 /**
440 * If value "value" is already in set "set", remove it; otherwise, add it.
441 *
442 * @param value a value of any type
443 * @param set an Object which we use to mimic set semantics
444 */
445 $scope.toggleValueInSet = function(value, set) {
446 if (true == set[value]) {
447 delete set[value];
448 } else {
449 set[value] = true;
450 }
451 }
452
epoger@google.comf4394d52013-10-29 15:49:40 +0000453 /**
454 * For each value in valueArray, call toggleValueInSet(value, set).
455 *
456 * @param valueArray
457 * @param set
458 */
459 $scope.toggleValuesInSet = function(valueArray, set) {
460 var arrayLength = valueArray.length;
461 for (var i = 0; i < arrayLength; i++) {
462 $scope.toggleValueInSet(valueArray[i], set);
463 }
464 }
465
epoger@google.comad0e5522013-10-24 15:38:27 +0000466
467 //
468 // Array operations; similar to our Set operations, but operate on a
469 // Javascript Array so we *can* easily get a list of all values in the Set.
470 // TODO(epoger): move into a separate .js file?
471 //
472
473 /**
474 * Returns true if value "value" is present within array "array".
475 *
476 * @param value a value of any type
477 * @param array a Javascript Array
478 */
479 $scope.isValueInArray = function(value, array) {
480 return (-1 != array.indexOf(value));
481 }
482
483 /**
484 * If value "value" is already in array "array", remove it; otherwise,
485 * add it.
486 *
487 * @param value a value of any type
488 * @param array a Javascript Array
489 */
490 $scope.toggleValueInArray = function(value, array) {
491 var i = array.indexOf(value);
492 if (-1 == i) {
493 array.push(value);
494 } else {
495 array.splice(i, 1);
496 }
497 }
498
499
500 //
501 // Miscellaneous utility functions.
502 // TODO(epoger): move into a separate .js file?
503 //
504
505 /**
506 * Returns a human-readable (in local time zone) time string for a
507 * particular moment in time.
508 *
509 * @param secondsPastEpoch (numeric): seconds past epoch in UTC
510 */
511 $scope.localTimeString = function(secondsPastEpoch) {
512 var d = new Date(secondsPastEpoch * 1000);
513 return d.toString();
514 }
515
epoger@google.comf9d134d2013-09-27 15:02:44 +0000516 }
517);