blob: 8cfdfecb82b316e98bae8e7fd158d849797aec27 [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,
20 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]) &&
29 (viewingTab == item.tab)) {
epoger@google.com9fb6c8a2013-10-09 18:05:58 +000030 filteredItems.push(item);
31 }
epoger@google.com5f2bb002013-10-02 18:57:48 +000032 }
33 return filteredItems;
34 };
35 }
36);
37
epoger@google.comad0e5522013-10-24 15:38:27 +000038
epoger@google.comf9d134d2013-09-27 15:02:44 +000039Loader.controller(
40 'Loader.Controller',
epoger@google.com542b65f2013-10-15 20:10:33 +000041 function($scope, $http, $filter, $location) {
42 $scope.windowTitle = "Loading GM Results...";
epoger@google.comdcb4e652013-10-11 18:45:33 +000043 var resultsToLoad = $location.search().resultsToLoad;
44 $scope.loadingMessage = "Loading results of type '" + resultsToLoad +
45 "', please wait...";
46
epoger@google.comad0e5522013-10-24 15:38:27 +000047 /**
48 * On initial page load, load a full dictionary of results.
49 * Once the dictionary is loaded, unhide the page elements so they can
50 * render the data.
51 */
epoger@google.comdcb4e652013-10-11 18:45:33 +000052 $http.get("/results/" + resultsToLoad).success(
53 function(data, status, header, config) {
54 $scope.loadingMessage = "Processing data, please wait...";
55
56 $scope.header = data.header;
57 $scope.categories = data.categories;
58 $scope.testData = data.testData;
epoger@google.comf9d134d2013-09-27 15:02:44 +000059 $scope.sortColumn = 'test';
epoger@google.comeb832592013-10-23 15:07:26 +000060 $scope.showTodos = false;
epoger@google.com5f2bb002013-10-02 18:57:48 +000061
epoger@google.com055e3b52013-10-26 14:31:11 +000062 $scope.showSubmitAdvancedSettings = false;
63 $scope.submitAdvancedSettings = {};
64 $scope.submitAdvancedSettings['reviewed-by-human'] = true;
65 $scope.submitAdvancedSettings['ignore-failures'] = false;
66 $scope.submitAdvancedSettings['bug'] = '';
67
epoger@google.comeb832592013-10-23 15:07:26 +000068 // Create the list of tabs (lists into which the user can file each
69 // test). This may vary, depending on isEditable.
70 $scope.tabs = [
71 'Unfiled', 'Hidden'
72 ];
73 if (data.header.isEditable) {
74 $scope.tabs = $scope.tabs.concat(
75 ['Pending Approval']);
76 }
77 $scope.defaultTab = $scope.tabs[0];
78 $scope.viewingTab = $scope.defaultTab;
79
80 // Track the number of results on each tab.
81 $scope.numResultsPerTab = {};
82 for (var i = 0; i < $scope.tabs.length; i++) {
83 $scope.numResultsPerTab[$scope.tabs[i]] = 0;
84 }
85 $scope.numResultsPerTab[$scope.defaultTab] = $scope.testData.length;
86
87 // Add index and tab fields to all records.
epoger@google.comdcb4e652013-10-11 18:45:33 +000088 for (var i = 0; i < $scope.testData.length; i++) {
89 $scope.testData[i].index = i;
epoger@google.comeb832592013-10-23 15:07:26 +000090 $scope.testData[i].tab = $scope.defaultTab;
epoger@google.comdcb4e652013-10-11 18:45:33 +000091 }
92
epoger@google.com055e3b52013-10-26 14:31:11 +000093 // Arrays within which the user can toggle individual elements.
epoger@google.comad0e5522013-10-24 15:38:27 +000094 $scope.selectedItems = [];
95
epoger@google.com055e3b52013-10-26 14:31:11 +000096 // Sets within which the user can toggle individual elements.
epoger@google.com9fb6c8a2013-10-09 18:05:58 +000097 $scope.hiddenResultTypes = {
98 'failure-ignored': true,
99 'no-comparison': true,
100 'succeeded': true,
101 };
102 $scope.hiddenConfigs = {};
epoger@google.com5f2bb002013-10-02 18:57:48 +0000103
104 $scope.updateResults();
epoger@google.comdcb4e652013-10-11 18:45:33 +0000105 $scope.loadingMessage = "";
epoger@google.com542b65f2013-10-15 20:10:33 +0000106 $scope.windowTitle = "Current GM Results";
epoger@google.comdcb4e652013-10-11 18:45:33 +0000107 }
108 ).error(
109 function(data, status, header, config) {
110 $scope.loadingMessage = "Failed to load results of type '"
111 + resultsToLoad + "'";
epoger@google.com542b65f2013-10-15 20:10:33 +0000112 $scope.windowTitle = "Failed to Load GM Results";
epoger@google.comf9d134d2013-09-27 15:02:44 +0000113 }
114 );
epoger@google.com5f2bb002013-10-02 18:57:48 +0000115
epoger@google.com9fb6c8a2013-10-09 18:05:58 +0000116
epoger@google.comad0e5522013-10-24 15:38:27 +0000117 //
epoger@google.com055e3b52013-10-26 14:31:11 +0000118 // Select/Clear/Toggle all tests.
119 //
120
121 /**
122 * Select all currently showing tests.
123 */
124 $scope.selectAllItems = function() {
125 var numItemsShowing = $scope.limitedTestData.length;
126 for (var i = 0; i < numItemsShowing; i++) {
127 var index = $scope.limitedTestData[i].index;
128 if (!$scope.isValueInArray(index, $scope.selectedItems)) {
129 $scope.toggleValueInArray(index, $scope.selectedItems);
130 }
131 }
132 }
133
134 /**
135 * Deselect all currently showing tests.
136 */
137 $scope.clearAllItems = function() {
138 var numItemsShowing = $scope.limitedTestData.length;
139 for (var i = 0; i < numItemsShowing; i++) {
140 var index = $scope.limitedTestData[i].index;
141 if ($scope.isValueInArray(index, $scope.selectedItems)) {
142 $scope.toggleValueInArray(index, $scope.selectedItems);
143 }
144 }
145 }
146
147 /**
148 * Toggle selection of all currently showing tests.
149 */
150 $scope.toggleAllItems = function() {
151 var numItemsShowing = $scope.limitedTestData.length;
152 for (var i = 0; i < numItemsShowing; i++) {
153 var index = $scope.limitedTestData[i].index;
154 $scope.toggleValueInArray(index, $scope.selectedItems);
155 }
156 }
157
158
159 //
epoger@google.comad0e5522013-10-24 15:38:27 +0000160 // Tab operations.
161 //
epoger@google.com5f2bb002013-10-02 18:57:48 +0000162
epoger@google.comad0e5522013-10-24 15:38:27 +0000163 /**
164 * Change the selected tab.
165 *
166 * @param tab (string): name of the tab to select
167 */
epoger@google.comeb832592013-10-23 15:07:26 +0000168 $scope.setViewingTab = function(tab) {
169 $scope.viewingTab = tab;
170 $scope.updateResults();
171 }
172
epoger@google.comeb832592013-10-23 15:07:26 +0000173 /**
174 * Move the items in $scope.selectedItems to a different tab,
175 * and then clear $scope.selectedItems.
176 *
177 * @param newTab (string): name of the tab to move the tests to
178 */
179 $scope.moveSelectedItemsToTab = function(newTab) {
180 $scope.moveItemsToTab($scope.selectedItems, newTab);
181 $scope.selectedItems = [];
182 $scope.updateResults();
183 }
184
185 /**
186 * Move a subset of $scope.testData to a different tab.
187 *
188 * @param itemIndices (array of ints): indices into $scope.testData
189 * indicating which test results to move
190 * @param newTab (string): name of the tab to move the tests to
191 */
192 $scope.moveItemsToTab = function(itemIndices, newTab) {
193 var itemIndex;
194 var numItems = itemIndices.length;
195 for (var i = 0; i < numItems; i++) {
196 itemIndex = itemIndices[i];
197 $scope.numResultsPerTab[$scope.testData[itemIndex].tab]--;
198 $scope.testData[itemIndex].tab = newTab;
199 }
200 $scope.numResultsPerTab[newTab] += numItems;
201 }
202
epoger@google.comad0e5522013-10-24 15:38:27 +0000203
204 //
205 // updateResults() and friends.
206 //
207
208 /**
209 * Set $scope.areUpdatesPending (to enable/disable the Update Results
210 * button).
211 *
212 * TODO(epoger): We could reduce the amount of code by just setting the
213 * variable directly (from, e.g., a button's ng-click handler). But when
214 * I tried that, the HTML elements depending on the variable did not get
215 * updated.
216 * It turns out that this is due to variable scoping within an ng-repeat
217 * element; see http://stackoverflow.com/questions/15388344/behavior-of-assignment-expression-invoked-by-ng-click-within-ng-repeat
218 *
219 * @param val boolean value to set $scope.areUpdatesPending to
220 */
221 $scope.setUpdatesPending = function(val) {
222 $scope.areUpdatesPending = val;
223 }
224
225 /**
226 * Update the displayed results, based on filters/settings.
227 */
epoger@google.com5f2bb002013-10-02 18:57:48 +0000228 $scope.updateResults = function() {
229 $scope.displayLimit = $scope.displayLimitPending;
230 // TODO(epoger): Every time we apply a filter, AngularJS creates
231 // another copy of the array. Is there a way we can filter out
232 // the items as they are displayed, rather than storing multiple
233 // array copies? (For better performance.)
epoger@google.comeb832592013-10-23 15:07:26 +0000234
235 if ($scope.viewingTab == $scope.defaultTab) {
236 $scope.filteredTestData =
237 $filter("orderBy")(
238 $filter("removeHiddenItems")(
239 $scope.testData,
240 $scope.hiddenResultTypes,
241 $scope.hiddenConfigs,
242 $scope.viewingTab
243 ),
244 $scope.sortColumn);
245 $scope.limitedTestData = $filter("limitTo")(
246 $scope.filteredTestData, $scope.displayLimit);
247 } else {
248 $scope.filteredTestData =
249 $filter("orderBy")(
250 $filter("filter")(
251 $scope.testData,
252 {tab: $scope.viewingTab},
253 true
254 ),
255 $scope.sortColumn);
epoger@google.com055e3b52013-10-26 14:31:11 +0000256 $scope.limitedTestData = $scope.filteredTestData;
epoger@google.comeb832592013-10-23 15:07:26 +0000257 }
epoger@google.com5f2bb002013-10-02 18:57:48 +0000258 $scope.imageSize = $scope.imageSizePending;
epoger@google.comad0e5522013-10-24 15:38:27 +0000259 $scope.setUpdatesPending(false);
epoger@google.com5f2bb002013-10-02 18:57:48 +0000260 }
261
epoger@google.comad0e5522013-10-24 15:38:27 +0000262 /**
263 * Re-sort the displayed results.
264 *
265 * @param sortColumn (string): name of the column to sort on
266 */
epoger@google.com5f2bb002013-10-02 18:57:48 +0000267 $scope.sortResultsBy = function(sortColumn) {
268 $scope.sortColumn = sortColumn;
269 $scope.updateResults();
270 }
epoger@google.comeb832592013-10-23 15:07:26 +0000271
epoger@google.comad0e5522013-10-24 15:38:27 +0000272
273 //
274 // Operations for sending info back to the server.
275 //
276
epoger@google.comeb832592013-10-23 15:07:26 +0000277 /**
278 * Tell the server that the actual results of these particular tests
279 * are acceptable.
280 *
281 * @param testDataSubset an array of test results, most likely a subset of
282 * $scope.testData (perhaps with some modifications)
283 */
284 $scope.submitApprovals = function(testDataSubset) {
285 $scope.submitPending = true;
epoger@google.com055e3b52013-10-26 14:31:11 +0000286
287 // Convert bug text field to null or 1-item array.
288 var bugs = null;
289 var bugNumber = parseInt($scope.submitAdvancedSettings['bug']);
290 if (!isNaN(bugNumber)) {
291 bugs = [bugNumber];
292 }
293
294 // TODO(epoger): This is a suboptimal way to prevent users from
295 // rebaselining failures in alternative renderModes, but it does work.
296 // For a better solution, see
297 // https://code.google.com/p/skia/issues/detail?id=1748 ('gm: add new
298 // result type, RenderModeMismatch')
299 var encounteredComparisonConfig = false;
300
epoger@google.comeb832592013-10-23 15:07:26 +0000301 var newResults = [];
302 for (var i = 0; i < testDataSubset.length; i++) {
303 var actualResult = testDataSubset[i];
304 var expectedResult = {
305 builder: actualResult['builder'],
306 test: actualResult['test'],
307 config: actualResult['config'],
308 expectedHashType: actualResult['actualHashType'],
309 expectedHashDigest: actualResult['actualHashDigest'],
310 };
epoger@google.com055e3b52013-10-26 14:31:11 +0000311 if (0 == expectedResult.config.indexOf('comparison-')) {
312 encounteredComparisonConfig = true;
313 }
314
315 // Advanced settings...
316 expectedResult['reviewed-by-human'] =
317 $scope.submitAdvancedSettings['reviewed-by-human'];
318 if (true == $scope.submitAdvancedSettings['ignore-failure']) {
319 // if it's false, don't send it at all (just keep the default)
320 expectedResult['ignoreFailure'] = true;
321 }
322 expectedResult['bugs'] = bugs;
323
epoger@google.comeb832592013-10-23 15:07:26 +0000324 newResults.push(expectedResult);
325 }
epoger@google.com055e3b52013-10-26 14:31:11 +0000326 if (encounteredComparisonConfig) {
327 alert("Approval failed -- you cannot approve results with config " +
328 "type comparison-*");
329 $scope.submitPending = false;
330 return;
331 }
epoger@google.comeb832592013-10-23 15:07:26 +0000332 $http({
333 method: "POST",
334 url: "/edits",
335 data: {
336 oldResultsType: $scope.header.type,
337 oldResultsHash: $scope.header.dataHash,
338 modifications: newResults
339 }
340 }).success(function(data, status, headers, config) {
341 var itemIndicesToMove = [];
342 for (var i = 0; i < testDataSubset.length; i++) {
343 itemIndicesToMove.push(testDataSubset[i].index);
344 }
345 $scope.moveItemsToTab(itemIndicesToMove,
346 "HackToMakeSureThisItemDisappears");
347 $scope.updateResults();
348 alert("New baselines submitted successfully!\n\n" +
349 "You still need to commit the updated expectations files on " +
350 "the server side to the Skia repo.\n\n" +
351 "Also: in order to see the complete updated data, or to submit " +
352 "more baselines, you will need to reload your client.");
353 $scope.submitPending = false;
354 }).error(function(data, status, headers, config) {
355 alert("There was an error submitting your baselines.\n\n" +
356 "Please see server-side log for details.");
357 $scope.submitPending = false;
358 });
359 }
epoger@google.comad0e5522013-10-24 15:38:27 +0000360
361
362 //
363 // Operations we use to mimic Set semantics, in such a way that
364 // checking for presence within the Set is as fast as possible.
365 // But getting a list of all values within the Set is not necessarily
366 // possible.
367 // TODO(epoger): move into a separate .js file?
368 //
369
370 /**
371 * Returns true if value "value" is present within set "set".
372 *
373 * @param value a value of any type
374 * @param set an Object which we use to mimic set semantics
375 * (this should make isValueInSet faster than if we used an Array)
376 */
377 $scope.isValueInSet = function(value, set) {
378 return (true == set[value]);
379 }
380
381 /**
382 * If value "value" is already in set "set", remove it; otherwise, add it.
383 *
384 * @param value a value of any type
385 * @param set an Object which we use to mimic set semantics
386 */
387 $scope.toggleValueInSet = function(value, set) {
388 if (true == set[value]) {
389 delete set[value];
390 } else {
391 set[value] = true;
392 }
393 }
394
395
396 //
397 // Array operations; similar to our Set operations, but operate on a
398 // Javascript Array so we *can* easily get a list of all values in the Set.
399 // TODO(epoger): move into a separate .js file?
400 //
401
402 /**
403 * Returns true if value "value" is present within array "array".
404 *
405 * @param value a value of any type
406 * @param array a Javascript Array
407 */
408 $scope.isValueInArray = function(value, array) {
409 return (-1 != array.indexOf(value));
410 }
411
412 /**
413 * If value "value" is already in array "array", remove it; otherwise,
414 * add it.
415 *
416 * @param value a value of any type
417 * @param array a Javascript Array
418 */
419 $scope.toggleValueInArray = function(value, array) {
420 var i = array.indexOf(value);
421 if (-1 == i) {
422 array.push(value);
423 } else {
424 array.splice(i, 1);
425 }
426 }
427
428
429 //
430 // Miscellaneous utility functions.
431 // TODO(epoger): move into a separate .js file?
432 //
433
434 /**
435 * Returns a human-readable (in local time zone) time string for a
436 * particular moment in time.
437 *
438 * @param secondsPastEpoch (numeric): seconds past epoch in UTC
439 */
440 $scope.localTimeString = function(secondsPastEpoch) {
441 var d = new Date(secondsPastEpoch * 1000);
442 return d.toString();
443 }
444
epoger@google.comf9d134d2013-09-27 15:02:44 +0000445 }
446);