diff options
author | epoger@google.com <epoger@google.com@2bbb7eff-a529-9590-31e7-b0007b416f81> | 2013-09-30 15:06:25 +0000 |
---|---|---|
committer | epoger@google.com <epoger@google.com@2bbb7eff-a529-9590-31e7-b0007b416f81> | 2013-09-30 15:06:25 +0000 |
commit | afaad3dd7002feb6e69983ddc4e5148d7803baed (patch) | |
tree | 9933a754bc7a94efa3d88ff348d8647e91a758ed | |
parent | 5e49738c99f573b2e5ba342f6ec8bfa3bcf125e4 (diff) |
HTTP GM results viewer: server now returns category summaries along with testData
(SkipBuildbotRuns)
R=borenet@google.com
Review URL: https://codereview.chromium.org/25045003
git-svn-id: http://skia.googlecode.com/svn/trunk@11520 2bbb7eff-a529-9590-31e7-b0007b416f81
-rwxr-xr-x | gm/rebaseline_server/results.py | 119 | ||||
-rwxr-xr-x | gm/rebaseline_server/server.py | 8 | ||||
-rw-r--r-- | gm/rebaseline_server/static/loader.js | 8 | ||||
-rw-r--r-- | gm/rebaseline_server/static/view.html | 55 |
4 files changed, 129 insertions, 61 deletions
diff --git a/gm/rebaseline_server/results.py b/gm/rebaseline_server/results.py index 0c50d26397..1336097f1f 100755 --- a/gm/rebaseline_server/results.py +++ b/gm/rebaseline_server/results.py @@ -31,6 +31,9 @@ if GM_DIRECTORY not in sys.path: import gm_json IMAGE_FILENAME_RE = re.compile(gm_json.IMAGE_FILENAME_PATTERN) +CATEGORIES_TO_SUMMARIZE = [ + 'builder', 'test', 'config', 'resultType', +] class Results(object): """ Loads actual and expected results from all builders, supplying combined @@ -44,24 +47,49 @@ class Results(object): """ self._actual_builder_dicts = Results._GetDictsFromRoot(actuals_root) self._expected_builder_dicts = Results._GetDictsFromRoot(expected_root) - self._all_results = self._Combine() + self._all_results = Results._Combine( + actual_builder_dicts=self._actual_builder_dicts, + expected_builder_dicts=self._expected_builder_dicts) def GetAll(self): - """Return results of all tests, as a list in this form: + """Return results of all tests, as a dictionary in this form: - [ + { + "categories": # dictionary of categories listed in + # CATEGORIES_TO_SUMMARIZE, with the number of times + # each value appears within its category { - "builder": "Test-Mac10.6-MacMini4.1-GeForce320M-x86-Debug", - "test": "bigmatrix", - "config": "8888", - "resultType": "failed", - "expectedHashType": "bitmap-64bitMD5", - "expectedHashDigest": "10894408024079689926", - "actualHashType": "bitmap-64bitMD5", - "actualHashDigest": "2409857384569", - }, - ... - ] + "resultType": # category name + { + "failed": 29, # category value and total number found of that value + "failure-ignored": 948, + "no-comparison": 4502, + "succeeded": 38609, + }, + "builder": + { + "Test-Mac10.6-MacMini4.1-GeForce320M-x86-Debug": 1286, + "Test-Mac10.6-MacMini4.1-GeForce320M-x86-Release": 1134, + ... + }, + ... # other categories from CATEGORIES_TO_SUMMARIZE + }, # end of "categories" dictionary + + "testData": # list of test results, with a dictionary for each + [ + { + "builder": "Test-Mac10.6-MacMini4.1-GeForce320M-x86-Debug", + "test": "bigmatrix", + "config": "8888", + "resultType": "failed", + "expectedHashType": "bitmap-64bitMD5", + "expectedHashDigest": "10894408024079689926", + "actualHashType": "bitmap-64bitMD5", + "actualHashDigest": "2409857384569", + }, + ... + ], # end of "testData" list + } """ return self._all_results @@ -84,15 +112,21 @@ class Results(object): meta_dict[builder] = gm_json.LoadFromFile(fullpath) return meta_dict - def _Combine(self): - """Returns a list of all tests, across all builders, based on the - contents of self._actual_builder_dicts and self._expected_builder_dicts . - Returns the list in the same form needed for GetAllResults(). + @staticmethod + def _Combine(actual_builder_dicts, expected_builder_dicts): + """Gathers the results of all tests, across all builders (based on the + contents of actual_builder_dicts and expected_builder_dicts) + and returns it in a list in the same form needed for self.GetAll(). + + This is a static method, because once we start refreshing results + asynchronously, we need to make sure we are not corrupting the object's + member variables. """ - all_tests = [] - for builder in sorted(self._actual_builder_dicts.keys()): + test_data = [] + category_dict = {} + for builder in sorted(actual_builder_dicts.keys()): actual_results_for_this_builder = ( - self._actual_builder_dicts[builder][gm_json.JSONKEY_ACTUALRESULTS]) + actual_builder_dicts[builder][gm_json.JSONKEY_ACTUALRESULTS]) for result_type in sorted(actual_results_for_this_builder.keys()): results_of_this_type = actual_results_for_this_builder[result_type] if not results_of_this_type: @@ -102,7 +136,7 @@ class Results(object): try: # TODO(epoger): assumes a single allowed digest per test expected_image = ( - self._expected_builder_dicts + expected_builder_dicts [builder][gm_json.JSONKEY_EXPECTEDRESULTS] [image_name][gm_json.JSONKEY_EXPECTEDRESULTS_ALLOWEDDIGESTS] [0]) @@ -159,13 +193,8 @@ class Results(object): else: updated_result_type = result_type - # TODO(epoger): For now, don't include succeeded results. - # There are so many of them that they make the client too slow. - if updated_result_type == gm_json.JSONKEY_ACTUALRESULTS_SUCCEEDED: - continue - (test, config) = IMAGE_FILENAME_RE.match(image_name).groups() - all_tests.append({ + results_for_this_test = { "builder": builder, "test": test, "config": config, @@ -174,5 +203,35 @@ class Results(object): "actualHashDigest": str(actual_image[1]), "expectedHashType": expected_image[0], "expectedHashDigest": str(expected_image[1]), - }) - return all_tests + } + Results._AddToCategoryDict(category_dict, results_for_this_test) + + # TODO(epoger): For now, don't include succeeded results in the raw + # data. There are so many of them that they make the client too slow. + if updated_result_type != gm_json.JSONKEY_ACTUALRESULTS_SUCCEEDED: + test_data.append(results_for_this_test) + return {"categories": category_dict, "testData": test_data} + + @staticmethod + def _AddToCategoryDict(category_dict, test_results): + """Add test_results to the category dictionary we are building + (see documentation of self.GetAll() for the format of this dictionary). + + params: + category_dict: category dict-of-dicts to add to; modify this in-place + test_results: test data with which to update category_list, in a dict: + { + "category_name": "category_value", + "category_name": "category_value", + ... + } + """ + for category in CATEGORIES_TO_SUMMARIZE: + category_value = test_results.get(category) + if not category_value: + continue # test_results did not include this category, keep going + if not category_dict.get(category): + category_dict[category] = {} + if not category_dict[category].get(category_value): + category_dict[category][category_value] = 0 + category_dict[category][category_value] += 1 diff --git a/gm/rebaseline_server/server.py b/gm/rebaseline_server/server.py index 5b81d8c2e2..34c70f4f4d 100755 --- a/gm/rebaseline_server/server.py +++ b/gm/rebaseline_server/server.py @@ -214,10 +214,10 @@ def main(): 'to access this server. WARNING: doing so will ' 'allow users on other hosts to modify your ' 'GM expectations!')) - parser.add_argument('--port', - help=('Which TCP port to listen on for HTTP requests; ' - 'defaults to %(default)s'), - default=DEFAULT_PORT) + parser.add_argument('--port', type=int, + help=('Which TCP port to listen on for HTTP requests; ' + 'defaults to %(default)s'), + default=DEFAULT_PORT) args = parser.parse_args() global _SERVER _SERVER = Server(expectations_dir=args.expectations_dir, diff --git a/gm/rebaseline_server/static/loader.js b/gm/rebaseline_server/static/loader.js index 68da73afda..d08fdccbf6 100644 --- a/gm/rebaseline_server/static/loader.js +++ b/gm/rebaseline_server/static/loader.js @@ -1,7 +1,7 @@ /* * Loader: - * Reads GM result reports written out by results_loader.py, and imports - * their data into $scope.results . + * Reads GM result reports written out by results.py, and imports + * them into $scope.categories and $scope.testData . */ var Loader = angular.module( 'Loader', @@ -12,8 +12,10 @@ Loader.controller( function($scope, $http) { $http.get("/results/all").then( function(response) { - $scope.results = response.data; + $scope.categories = response.data.categories; + $scope.testData = response.data.testData; $scope.sortColumn = 'test'; + $scope.showResultsOfType = 'failed'; } ); } diff --git a/gm/rebaseline_server/static/view.html b/gm/rebaseline_server/static/view.html index 89ef538271..5913e5c2ca 100644 --- a/gm/rebaseline_server/static/view.html +++ b/gm/rebaseline_server/static/view.html @@ -15,6 +15,9 @@ --export mode --> + <!-- TODO(epoger): Add some indication of how old the + expected/actual data is --> + Settings: <ul> <!-- TODO(epoger): Now that we get multiple result types in a single @@ -24,45 +27,49 @@ <li>show results of type <select ng-model="showResultsOfType" ng-init="showResultsOfType='failed'"> - <option>failed</option> - <option>failure-ignored</option> - <!-- - <option>no-comparison</option> - - TODO(epoger): For now, I have disabled viewing the - no-comparison results because there are so many of them, and - the browser takes forever to download all the images. Maybe - we should use some sort of lazy-loading technique - (e.g. http://www.appelsiini.net/projects/lazyload ), so that - the images are only loaded as they become viewable... - --> - <!-- - <option>succeeded</option> - + <option ng-repeat="(resultType, count) in categories['resultType']" + value="{{resultType}}"> + {{resultType}} ({{count}}) + </option> + </select> + <!-- TODO(epoger): See results.py: for now, I have disabled returning succeeded tests as part of the JSON, because it makes the returned JSON too big (and slows down the client). - --> - </select> + + Also, we should use some sort of lazy-loading technique + (e.g. http://www.appelsiini.net/projects/lazyload ), so that + the images are only loaded as they become viewable... + that will help with long lists like resultType='no-comparison'. + --> + <br> + TODO(epoger): 'no-comparison' will probably take forever; + see HTML source for details + <br> + TODO(epoger): 'succeeded' will not show any results; + see HTML source for details </li> <li>image size - <input type="text" ng-model="imageSize" ng-init="imageSize=100" + <input type="text" ng-model="imageSizePending" + ng-init="imageSizePending=100; imageSize=100" maxlength="4"/> + <button ng:click="imageSize=imageSizePending">apply</button> </li> </ul> <p> + Click on the column header radio buttons to re-sort by that column...<br> <!-- TODO(epoger): Show some sort of "loading" message, instead of an empty table, while the data is loading. Otherwise, if there are a lot of failures and it takes a long time to load them, the user might think there are NO failures and leave the page! --> <table border="1"> <tr> - <th ng:click="sortColumn='builder'">Builder</th> - <th ng:click="sortColumn='test'">Test</th> - <th ng:click="sortColumn='config'">Config</th> - <th ng:click="sortColumn='expectedHashDigest'">Expected Image</th> - <th ng:click="sortColumn='actualHashDigest'">Actual Image</th> + <th><input ng-model="sortColumn" name="sortColumnRadio" type="radio" value="builder">Builder</input></th> + <th><input ng-model="sortColumn" name="sortColumnRadio" type="radio" value="test">Test</input></th> + <th><input ng-model="sortColumn" name="sortColumnRadio" type="radio" value="config">Config</input></th> + <th><input ng-model="sortColumn" name="sortColumnRadio" type="radio" value="expectedHashDigest">Expected Image</input></th> + <th><input ng-model="sortColumn" name="sortColumnRadio" type="radio" value="actualHashDigest">Actual Image</input></th> <!-- TODO(epoger): Add more columns, such as... pixel diff notes/bugs @@ -71,7 +78,7 @@ </tr> <!-- TODO(epoger): improve the column sorting, as per http://jsfiddle.net/vojtajina/js64b/14/ --> - <tr ng-repeat="result in results | filter: { resultType: showResultsOfType } | orderBy: sortColumn"> + <tr ng-repeat="result in testData | filter: { resultType: showResultsOfType } | orderBy: sortColumn"> <td>{{result.builder}}</td> <td>{{result.test}}</td> <td>{{result.config}}</td> |