blob: 04b023651af0bfffeda1ceb6e61335a25af73626 [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.comf9d134d2013-09-27 15:02:44 +000061 $scope.sortColumn = 'test';
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;
67 $scope.submitAdvancedSettings['ignore-failures'] = false;
68 $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) {
245 $scope.filteredTestData =
246 $filter("orderBy")(
247 $filter("removeHiddenItems")(
248 $scope.testData,
249 $scope.hiddenResultTypes,
250 $scope.hiddenConfigs,
epoger@google.comf4394d52013-10-29 15:49:40 +0000251 $scope.categoryValueMatch.builder,
252 $scope.categoryValueMatch.test,
epoger@google.comeb832592013-10-23 15:07:26 +0000253 $scope.viewingTab
254 ),
255 $scope.sortColumn);
256 $scope.limitedTestData = $filter("limitTo")(
257 $scope.filteredTestData, $scope.displayLimit);
258 } else {
259 $scope.filteredTestData =
260 $filter("orderBy")(
261 $filter("filter")(
262 $scope.testData,
263 {tab: $scope.viewingTab},
264 true
265 ),
266 $scope.sortColumn);
epoger@google.com055e3b52013-10-26 14:31:11 +0000267 $scope.limitedTestData = $scope.filteredTestData;
epoger@google.comeb832592013-10-23 15:07:26 +0000268 }
epoger@google.com5f2bb002013-10-02 18:57:48 +0000269 $scope.imageSize = $scope.imageSizePending;
epoger@google.comad0e5522013-10-24 15:38:27 +0000270 $scope.setUpdatesPending(false);
epoger@google.com5f2bb002013-10-02 18:57:48 +0000271 }
272
epoger@google.comad0e5522013-10-24 15:38:27 +0000273 /**
274 * Re-sort the displayed results.
275 *
276 * @param sortColumn (string): name of the column to sort on
277 */
epoger@google.com5f2bb002013-10-02 18:57:48 +0000278 $scope.sortResultsBy = function(sortColumn) {
279 $scope.sortColumn = sortColumn;
280 $scope.updateResults();
281 }
epoger@google.comeb832592013-10-23 15:07:26 +0000282
epoger@google.comf4394d52013-10-29 15:49:40 +0000283 /**
284 * Set $scope.categoryValueMatch[name] = value, and update results.
285 *
286 * @param name
287 * @param value
288 */
289 $scope.setCategoryValueMatch = function(name, value) {
290 $scope.categoryValueMatch[name] = value;
291 $scope.updateResults();
292 }
293
294 /**
295 * Update $scope.hiddenResultTypes so that ONLY this resultType is showing,
296 * and update the visible results.
297 *
298 * @param resultType
299 */
300 $scope.showOnlyResultType = function(resultType) {
301 $scope.hiddenResultTypes = {};
302 // TODO(epoger): Maybe change $scope.allResultTypes to be a Set like
303 // $scope.hiddenResultTypes (rather than an array), so this operation is
304 // simpler (just assign or add allResultTypes to hiddenResultTypes).
305 $scope.toggleValuesInSet($scope.allResultTypes, $scope.hiddenResultTypes);
306 $scope.toggleValueInSet(resultType, $scope.hiddenResultTypes);
307 $scope.updateResults();
308 }
309
310 /**
311 * Update $scope.hiddenConfigs so that ONLY this config is showing,
312 * and update the visible results.
313 *
314 * @param config
315 */
316 $scope.showOnlyConfig = function(config) {
317 $scope.hiddenConfigs = {};
318 $scope.toggleValuesInSet($scope.allConfigs, $scope.hiddenConfigs);
319 $scope.toggleValueInSet(config, $scope.hiddenConfigs);
320 $scope.updateResults();
321 }
322
epoger@google.comad0e5522013-10-24 15:38:27 +0000323
324 //
325 // Operations for sending info back to the server.
326 //
327
epoger@google.comeb832592013-10-23 15:07:26 +0000328 /**
329 * Tell the server that the actual results of these particular tests
330 * are acceptable.
331 *
332 * @param testDataSubset an array of test results, most likely a subset of
333 * $scope.testData (perhaps with some modifications)
334 */
335 $scope.submitApprovals = function(testDataSubset) {
336 $scope.submitPending = true;
epoger@google.com055e3b52013-10-26 14:31:11 +0000337
338 // Convert bug text field to null or 1-item array.
339 var bugs = null;
340 var bugNumber = parseInt($scope.submitAdvancedSettings['bug']);
341 if (!isNaN(bugNumber)) {
342 bugs = [bugNumber];
343 }
344
345 // TODO(epoger): This is a suboptimal way to prevent users from
346 // rebaselining failures in alternative renderModes, but it does work.
347 // For a better solution, see
348 // https://code.google.com/p/skia/issues/detail?id=1748 ('gm: add new
349 // result type, RenderModeMismatch')
350 var encounteredComparisonConfig = false;
351
epoger@google.comeb832592013-10-23 15:07:26 +0000352 var newResults = [];
353 for (var i = 0; i < testDataSubset.length; i++) {
354 var actualResult = testDataSubset[i];
355 var expectedResult = {
356 builder: actualResult['builder'],
357 test: actualResult['test'],
358 config: actualResult['config'],
359 expectedHashType: actualResult['actualHashType'],
360 expectedHashDigest: actualResult['actualHashDigest'],
361 };
epoger@google.com055e3b52013-10-26 14:31:11 +0000362 if (0 == expectedResult.config.indexOf('comparison-')) {
363 encounteredComparisonConfig = true;
364 }
365
366 // Advanced settings...
367 expectedResult['reviewed-by-human'] =
368 $scope.submitAdvancedSettings['reviewed-by-human'];
369 if (true == $scope.submitAdvancedSettings['ignore-failure']) {
370 // if it's false, don't send it at all (just keep the default)
371 expectedResult['ignoreFailure'] = true;
372 }
373 expectedResult['bugs'] = bugs;
374
epoger@google.comeb832592013-10-23 15:07:26 +0000375 newResults.push(expectedResult);
376 }
epoger@google.com055e3b52013-10-26 14:31:11 +0000377 if (encounteredComparisonConfig) {
378 alert("Approval failed -- you cannot approve results with config " +
379 "type comparison-*");
380 $scope.submitPending = false;
381 return;
382 }
epoger@google.comeb832592013-10-23 15:07:26 +0000383 $http({
384 method: "POST",
385 url: "/edits",
386 data: {
387 oldResultsType: $scope.header.type,
388 oldResultsHash: $scope.header.dataHash,
389 modifications: newResults
390 }
391 }).success(function(data, status, headers, config) {
392 var itemIndicesToMove = [];
393 for (var i = 0; i < testDataSubset.length; i++) {
394 itemIndicesToMove.push(testDataSubset[i].index);
395 }
396 $scope.moveItemsToTab(itemIndicesToMove,
397 "HackToMakeSureThisItemDisappears");
398 $scope.updateResults();
399 alert("New baselines submitted successfully!\n\n" +
400 "You still need to commit the updated expectations files on " +
401 "the server side to the Skia repo.\n\n" +
402 "Also: in order to see the complete updated data, or to submit " +
403 "more baselines, you will need to reload your client.");
404 $scope.submitPending = false;
405 }).error(function(data, status, headers, config) {
406 alert("There was an error submitting your baselines.\n\n" +
407 "Please see server-side log for details.");
408 $scope.submitPending = false;
409 });
410 }
epoger@google.comad0e5522013-10-24 15:38:27 +0000411
412
413 //
414 // Operations we use to mimic Set semantics, in such a way that
415 // checking for presence within the Set is as fast as possible.
416 // But getting a list of all values within the Set is not necessarily
417 // possible.
418 // TODO(epoger): move into a separate .js file?
419 //
420
421 /**
422 * Returns true if value "value" is present within set "set".
423 *
424 * @param value a value of any type
425 * @param set an Object which we use to mimic set semantics
426 * (this should make isValueInSet faster than if we used an Array)
427 */
428 $scope.isValueInSet = function(value, set) {
429 return (true == set[value]);
430 }
431
432 /**
433 * If value "value" is already in set "set", remove it; otherwise, add it.
434 *
435 * @param value a value of any type
436 * @param set an Object which we use to mimic set semantics
437 */
438 $scope.toggleValueInSet = function(value, set) {
439 if (true == set[value]) {
440 delete set[value];
441 } else {
442 set[value] = true;
443 }
444 }
445
epoger@google.comf4394d52013-10-29 15:49:40 +0000446 /**
447 * For each value in valueArray, call toggleValueInSet(value, set).
448 *
449 * @param valueArray
450 * @param set
451 */
452 $scope.toggleValuesInSet = function(valueArray, set) {
453 var arrayLength = valueArray.length;
454 for (var i = 0; i < arrayLength; i++) {
455 $scope.toggleValueInSet(valueArray[i], set);
456 }
457 }
458
epoger@google.comad0e5522013-10-24 15:38:27 +0000459
460 //
461 // Array operations; similar to our Set operations, but operate on a
462 // Javascript Array so we *can* easily get a list of all values in the Set.
463 // TODO(epoger): move into a separate .js file?
464 //
465
466 /**
467 * Returns true if value "value" is present within array "array".
468 *
469 * @param value a value of any type
470 * @param array a Javascript Array
471 */
472 $scope.isValueInArray = function(value, array) {
473 return (-1 != array.indexOf(value));
474 }
475
476 /**
477 * If value "value" is already in array "array", remove it; otherwise,
478 * add it.
479 *
480 * @param value a value of any type
481 * @param array a Javascript Array
482 */
483 $scope.toggleValueInArray = function(value, array) {
484 var i = array.indexOf(value);
485 if (-1 == i) {
486 array.push(value);
487 } else {
488 array.splice(i, 1);
489 }
490 }
491
492
493 //
494 // Miscellaneous utility functions.
495 // TODO(epoger): move into a separate .js file?
496 //
497
498 /**
499 * Returns a human-readable (in local time zone) time string for a
500 * particular moment in time.
501 *
502 * @param secondsPastEpoch (numeric): seconds past epoch in UTC
503 */
504 $scope.localTimeString = function(secondsPastEpoch) {
505 var d = new Date(secondsPastEpoch * 1000);
506 return d.toString();
507 }
508
epoger@google.comf9d134d2013-09-27 15:02:44 +0000509 }
510);