OLD | NEW |
---|---|
1 #!/usr/bin/python | 1 #!/usr/bin/python |
2 | 2 |
epoger
2014/03/13 14:37:04
Probably. If it's OK with you, I'll wait until I
| |
3 """ | 3 """ |
4 Copyright 2013 Google Inc. | 4 Copyright 2013 Google Inc. |
5 | 5 |
6 Use of this source code is governed by a BSD-style license that can be | 6 Use of this source code is governed by a BSD-style license that can be |
7 found in the LICENSE file. | 7 found in the LICENSE file. |
8 | 8 |
9 Repackage expected/actual GM results as needed by our HTML rebaseline viewer. | 9 Repackage expected/actual GM results as needed by our HTML rebaseline viewer. |
10 """ | 10 """ |
11 | 11 |
12 # System-level imports | 12 # System-level imports |
13 import argparse | 13 import argparse |
14 import fnmatch | 14 import fnmatch |
15 import json | 15 import json |
16 import logging | 16 import logging |
17 import os | 17 import os |
18 import re | 18 import re |
19 import sys | 19 import sys |
20 import time | 20 import time |
21 | 21 |
22 # Imports from within Skia | 22 # Imports from within Skia |
23 # | 23 # |
24 # We need to add the 'gm' directory, so that we can import gm_json.py within | 24 # We need to add the 'gm' directory, so that we can import gm_json.py within |
25 # that directory. That script allows us to parse the actual-results.json file | 25 # that directory. That script allows us to parse the actual-results.json file |
26 # written out by the GM tool. | 26 # written out by the GM tool. |
27 # Make sure that the 'gm' dir is in the PYTHONPATH, but add it at the *end* | 27 # Make sure that the 'gm' dir is in the PYTHONPATH, but add it at the *end* |
28 # so any dirs that are already in the PYTHONPATH will be preferred. | 28 # so any dirs that are already in the PYTHONPATH will be preferred. |
29 PARENT_DIRECTORY = os.path.dirname(os.path.realpath(__file__)) | 29 PARENT_DIRECTORY = os.path.dirname(os.path.realpath(__file__)) |
30 GM_DIRECTORY = os.path.dirname(PARENT_DIRECTORY) | 30 GM_DIRECTORY = os.path.dirname(PARENT_DIRECTORY) |
31 TRUNK_DIRECTORY = os.path.dirname(GM_DIRECTORY) | |
31 if GM_DIRECTORY not in sys.path: | 32 if GM_DIRECTORY not in sys.path: |
32 sys.path.append(GM_DIRECTORY) | 33 sys.path.append(GM_DIRECTORY) |
33 import gm_json | 34 import gm_json |
34 import imagediffdb | 35 import imagediffdb |
35 import imagepair | 36 import imagepair |
36 import imagepairset | 37 import imagepairset |
37 | 38 |
38 # Keys used to link an image to a particular GM test. | 39 # Keys used to link an image to a particular GM test. |
39 # NOTE: Keep these in sync with static/constants.js | 40 # NOTE: Keep these in sync with static/constants.js |
40 KEY__EXPECTATIONS__BUGS = gm_json.JSONKEY_EXPECTEDRESULTS_BUGS | 41 KEY__EXPECTATIONS__BUGS = gm_json.JSONKEY_EXPECTEDRESULTS_BUGS |
41 KEY__EXPECTATIONS__IGNOREFAILURE = gm_json.JSONKEY_EXPECTEDRESULTS_IGNOREFAILURE | 42 KEY__EXPECTATIONS__IGNOREFAILURE = gm_json.JSONKEY_EXPECTEDRESULTS_IGNOREFAILURE |
42 KEY__EXPECTATIONS__REVIEWED = gm_json.JSONKEY_EXPECTEDRESULTS_REVIEWED | 43 KEY__EXPECTATIONS__REVIEWED = gm_json.JSONKEY_EXPECTEDRESULTS_REVIEWED |
43 KEY__EXTRACOLUMN__BUILDER = 'builder' | 44 KEY__EXTRACOLUMN__BUILDER = 'builder' |
44 KEY__EXTRACOLUMN__CONFIG = 'config' | 45 KEY__EXTRACOLUMN__CONFIG = 'config' |
45 KEY__EXTRACOLUMN__RESULT_TYPE = 'resultType' | 46 KEY__EXTRACOLUMN__RESULT_TYPE = 'resultType' |
46 KEY__EXTRACOLUMN__TEST = 'test' | 47 KEY__EXTRACOLUMN__TEST = 'test' |
48 KEY__HEADER = 'header' | |
49 KEY__HEADER__DATAHASH = 'dataHash' | |
50 KEY__HEADER__IS_EDITABLE = 'isEditable' | |
51 KEY__HEADER__IS_EXPORTED = 'isExported' | |
52 KEY__HEADER__IS_STILL_LOADING = 'resultsStillLoading' | |
47 KEY__HEADER__RESULTS_ALL = 'all' | 53 KEY__HEADER__RESULTS_ALL = 'all' |
48 KEY__HEADER__RESULTS_FAILURES = 'failures' | 54 KEY__HEADER__RESULTS_FAILURES = 'failures' |
55 KEY__HEADER__TIME_NEXT_UPDATE_AVAILABLE = 'timeNextUpdateAvailable' | |
56 KEY__HEADER__TIME_UPDATED = 'timeUpdated' | |
57 KEY__HEADER__TYPE = 'type' | |
49 KEY__NEW_IMAGE_URL = 'newImageUrl' | 58 KEY__NEW_IMAGE_URL = 'newImageUrl' |
50 KEY__RESULT_TYPE__FAILED = gm_json.JSONKEY_ACTUALRESULTS_FAILED | 59 KEY__RESULT_TYPE__FAILED = gm_json.JSONKEY_ACTUALRESULTS_FAILED |
51 KEY__RESULT_TYPE__FAILUREIGNORED = gm_json.JSONKEY_ACTUALRESULTS_FAILUREIGNORED | 60 KEY__RESULT_TYPE__FAILUREIGNORED = gm_json.JSONKEY_ACTUALRESULTS_FAILUREIGNORED |
52 KEY__RESULT_TYPE__NOCOMPARISON = gm_json.JSONKEY_ACTUALRESULTS_NOCOMPARISON | 61 KEY__RESULT_TYPE__NOCOMPARISON = gm_json.JSONKEY_ACTUALRESULTS_NOCOMPARISON |
53 KEY__RESULT_TYPE__SUCCEEDED = gm_json.JSONKEY_ACTUALRESULTS_SUCCEEDED | 62 KEY__RESULT_TYPE__SUCCEEDED = gm_json.JSONKEY_ACTUALRESULTS_SUCCEEDED |
54 | 63 |
55 EXPECTATION_FIELDS_PASSED_THRU_VERBATIM = [ | 64 EXPECTATION_FIELDS_PASSED_THRU_VERBATIM = [ |
56 KEY__EXPECTATIONS__BUGS, | 65 KEY__EXPECTATIONS__BUGS, |
57 KEY__EXPECTATIONS__IGNOREFAILURE, | 66 KEY__EXPECTATIONS__IGNOREFAILURE, |
58 KEY__EXPECTATIONS__REVIEWED, | 67 KEY__EXPECTATIONS__REVIEWED, |
59 ] | 68 ] |
60 | 69 |
61 IMAGE_FILENAME_RE = re.compile(gm_json.IMAGE_FILENAME_PATTERN) | 70 IMAGE_FILENAME_RE = re.compile(gm_json.IMAGE_FILENAME_PATTERN) |
62 IMAGE_FILENAME_FORMATTER = '%s_%s.png' # pass in (testname, config) | 71 IMAGE_FILENAME_FORMATTER = '%s_%s.png' # pass in (testname, config) |
63 | 72 |
64 IMAGEPAIR_SET_DESCRIPTIONS = ('expected image', 'actual image') | 73 IMAGEPAIR_SET_DESCRIPTIONS = ('expected image', 'actual image') |
65 | 74 |
75 DEFAULT_ACTUALS_DIR = '.gm-actuals' | |
76 DEFAULT_EXPECTATIONS_DIR = os.path.join(TRUNK_DIRECTORY, 'expectations', 'gm') | |
77 DEFAULT_GENERATED_IMAGES_ROOT = os.path.join(PARENT_DIRECTORY, 'static', | |
78 'generated-images') | |
79 | |
66 | 80 |
67 class Results(object): | 81 class Results(object): |
68 """ Loads actual and expected GM results into an ImagePairSet. | 82 """ Loads actual and expected GM results into an ImagePairSet. |
69 | 83 |
70 Loads actual and expected results from all builders, except for those skipped | 84 Loads actual and expected results from all builders, except for those skipped |
71 by _ignore_builder(). | 85 by _ignore_builder(). |
72 | 86 |
73 Once this object has been constructed, the results (in self._results[]) | 87 Once this object has been constructed, the results (in self._results[]) |
74 are immutable. If you want to update the results based on updated JSON | 88 are immutable. If you want to update the results based on updated JSON |
75 file contents, you will need to create a new Results object.""" | 89 file contents, you will need to create a new Results object.""" |
76 | 90 |
77 def __init__(self, actuals_root, expected_root, generated_images_root): | 91 def __init__(self, actuals_root=DEFAULT_ACTUALS_DIR, |
92 expected_root=DEFAULT_EXPECTATIONS_DIR, | |
93 generated_images_root=DEFAULT_GENERATED_IMAGES_ROOT): | |
78 """ | 94 """ |
79 Args: | 95 Args: |
80 actuals_root: root directory containing all actual-results.json files | 96 actuals_root: root directory containing all actual-results.json files |
81 expected_root: root directory containing all expected-results.json files | 97 expected_root: root directory containing all expected-results.json files |
82 generated_images_root: directory within which to create all pixel diffs; | 98 generated_images_root: directory within which to create all pixel diffs; |
83 if this directory does not yet exist, it will be created | 99 if this directory does not yet exist, it will be created |
84 """ | 100 """ |
85 time_start = int(time.time()) | 101 time_start = int(time.time()) |
86 self._image_diff_db = imagediffdb.ImageDiffDB(generated_images_root) | 102 self._image_diff_db = imagediffdb.ImageDiffDB(generated_images_root) |
87 self._actuals_root = actuals_root | 103 self._actuals_root = actuals_root |
(...skipping 55 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
143 new_expectations[field] = value | 159 new_expectations[field] = value |
144 builder_dict = expected_builder_dicts[ | 160 builder_dict = expected_builder_dicts[ |
145 mod[imagepair.KEY__EXTRA_COLUMN_VALUES][KEY__EXTRACOLUMN__BUILDER]] | 161 mod[imagepair.KEY__EXTRA_COLUMN_VALUES][KEY__EXTRACOLUMN__BUILDER]] |
146 builder_expectations = builder_dict.get(gm_json.JSONKEY_EXPECTEDRESULTS) | 162 builder_expectations = builder_dict.get(gm_json.JSONKEY_EXPECTEDRESULTS) |
147 if not builder_expectations: | 163 if not builder_expectations: |
148 builder_expectations = {} | 164 builder_expectations = {} |
149 builder_dict[gm_json.JSONKEY_EXPECTEDRESULTS] = builder_expectations | 165 builder_dict[gm_json.JSONKEY_EXPECTEDRESULTS] = builder_expectations |
150 builder_expectations[image_name] = new_expectations | 166 builder_expectations[image_name] = new_expectations |
151 Results._write_dicts_to_root(expected_builder_dicts, self._expected_root) | 167 Results._write_dicts_to_root(expected_builder_dicts, self._expected_root) |
152 | 168 |
153 def get_results_of_type(self, type): | 169 def get_results_of_type(self, results_type): |
154 """Return results of some/all tests (depending on 'type' parameter). | 170 """Return results of some/all tests (depending on 'results_type' parameter). |
155 | 171 |
156 Args: | 172 Args: |
157 type: string describing which types of results to include; must be one | 173 results_type: string describing which types of results to include; must |
158 of the RESULTS_* constants | 174 be one of the RESULTS_* constants |
159 | 175 |
160 Results are returned in a dictionary as output by ImagePairSet.as_dict(). | 176 Results are returned in a dictionary as output by ImagePairSet.as_dict(). |
161 """ | 177 """ |
162 return self._results[type] | 178 return self._results[results_type] |
179 | |
180 def get_packaged_results_of_type(self, results_type, reload_seconds=None, | |
epoger
2014/03/12 21:08:35
moved here from server.py
| |
181 is_editable=False, is_exported=True): | |
182 """ Package the results of some/all tests as a complete response_dict. | |
183 | |
184 Args: | |
185 results_type: string indicating which set of results to return; | |
186 must be one of the RESULTS_* constants | |
187 reload_seconds: if specified, note that new results may be available once | |
188 these results are reload_seconds old | |
189 is_editable: whether clients are allowed to submit new baselines | |
190 is_exported: whether these results are being made available to other | |
191 network hosts | |
192 """ | |
193 response_dict = self._results[results_type] | |
194 time_updated = self.get_timestamp() | |
195 response_dict[KEY__HEADER] = { | |
196 # Timestamps: | |
197 # 1. when this data was last updated | |
198 # 2. when the caller should check back for new data (if ever) | |
199 KEY__HEADER__TIME_UPDATED: time_updated, | |
200 KEY__HEADER__TIME_NEXT_UPDATE_AVAILABLE: ( | |
201 (time_updated+reload_seconds) if reload_seconds else None), | |
202 | |
203 # The type we passed to get_results_of_type() | |
204 KEY__HEADER__TYPE: results_type, | |
205 | |
206 # Hash of dataset, which the client must return with any edits-- | |
207 # this ensures that the edits were made to a particular dataset. | |
208 KEY__HEADER__DATAHASH: str(hash(repr( | |
209 response_dict[imagepairset.KEY__IMAGEPAIRS]))), | |
210 | |
211 # Whether the server will accept edits back. | |
212 KEY__HEADER__IS_EDITABLE: is_editable, | |
213 | |
214 # Whether the service is accessible from other hosts. | |
215 KEY__HEADER__IS_EXPORTED: is_exported, | |
216 } | |
217 return response_dict | |
163 | 218 |
164 @staticmethod | 219 @staticmethod |
165 def _ignore_builder(builder): | 220 def _ignore_builder(builder): |
166 """Returns True if we should ignore expectations and actuals for a builder. | 221 """Returns True if we should ignore expectations and actuals for a builder. |
167 | 222 |
168 This allows us to ignore builders for which we don't maintain expectations | 223 This allows us to ignore builders for which we don't maintain expectations |
169 (trybots, Valgrind, ASAN, TSAN), and avoid problems like | 224 (trybots, Valgrind, ASAN, TSAN), and avoid problems like |
170 https://code.google.com/p/skia/issues/detail?id=2036 ('rebaseline_server | 225 https://code.google.com/p/skia/issues/detail?id=2036 ('rebaseline_server |
171 produces error when trying to add baselines for ASAN/TSAN builders') | 226 produces error when trying to add baselines for ASAN/TSAN builders') |
172 | 227 |
(...skipping 235 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
408 KEY__HEADER__RESULTS_FAILURES: failing_image_pairs.as_dict(), | 463 KEY__HEADER__RESULTS_FAILURES: failing_image_pairs.as_dict(), |
409 } | 464 } |
410 | 465 |
411 | 466 |
412 def main(): | 467 def main(): |
413 logging.basicConfig(format='%(asctime)s %(levelname)s %(message)s', | 468 logging.basicConfig(format='%(asctime)s %(levelname)s %(message)s', |
414 datefmt='%m/%d/%Y %H:%M:%S', | 469 datefmt='%m/%d/%Y %H:%M:%S', |
415 level=logging.INFO) | 470 level=logging.INFO) |
416 parser = argparse.ArgumentParser() | 471 parser = argparse.ArgumentParser() |
417 parser.add_argument( | 472 parser.add_argument( |
418 '--actuals', required=True, | 473 '--actuals', default=DEFAULT_ACTUALS_DIR, |
419 help='Directory containing all actual-result JSON files') | 474 help='Directory containing all actual-result JSON files') |
420 parser.add_argument( | 475 parser.add_argument( |
421 '--expectations', required=True, | 476 '--expectations', default=DEFAULT_EXPECTATIONS_DIR, |
422 help='Directory containing all expected-result JSON files') | 477 help='Directory containing all expected-result JSON files; defaults to ' |
478 '\'%(default)s\' .') | |
423 parser.add_argument( | 479 parser.add_argument( |
424 '--outfile', required=True, | 480 '--outfile', required=True, |
425 help='File to write result summary into, in JSON format') | 481 help='File to write result summary into, in JSON format.') |
426 parser.add_argument( | 482 parser.add_argument( |
427 '--workdir', default='.workdir', | 483 '--results', default=KEY__HEADER__RESULTS_FAILURES, |
428 help='Directory within which to download images and generate diffs') | 484 help='Which result types to include. Defaults to \'%(default)s\'; ' |
485 'must be one of ' + | |
486 str([KEY__HEADER__RESULTS_FAILURES, KEY__HEADER__RESULTS_ALL])) | |
487 parser.add_argument( | |
488 '--workdir', default=DEFAULT_GENERATED_IMAGES_ROOT, | |
489 help='Directory within which to download images and generate diffs; ' | |
490 'defaults to \'%(default)s\' .') | |
rmistry
2014/03/13 13:11:46
This is cool, I did not know you could do this to
| |
429 args = parser.parse_args() | 491 args = parser.parse_args() |
430 results = Results(actuals_root=args.actuals, | 492 results = Results(actuals_root=args.actuals, |
431 expected_root=args.expectations, | 493 expected_root=args.expectations, |
432 generated_images_root=args.workdir) | 494 generated_images_root=args.workdir) |
433 gm_json.WriteToFile(results.get_results_of_type(KEY__HEADER__RESULTS_ALL), | 495 gm_json.WriteToFile( |
434 args.outfile) | 496 results.get_packaged_results_of_type(results_type=args.results), |
497 args.outfile) | |
435 | 498 |
436 | 499 |
437 if __name__ == '__main__': | 500 if __name__ == '__main__': |
438 main() | 501 main() |
OLD | NEW |