OLD | NEW |
| (Empty) |
1 #!usr/bin/env python | |
2 # Copyright (c) 2006-2009 The Chromium Authors. All rights reserved. | |
3 # Use of this source code is governed by a BSD-style license that can be | |
4 # found in the LICENSE file. | |
5 | |
6 """Rebaselining tool that automatically produces baselines for all platforms. | |
7 | |
8 The script does the following for each platform specified: | |
9 1. Compile a list of tests that need rebaselining. | |
10 2. Download test result archive from buildbot for the platform. | |
11 3. Extract baselines from the archive file for all identified files. | |
12 4. Add new baselines to SVN repository. | |
13 5. For each test that has been rebaselined, remove this platform option from | |
14 the test in test_expectation.txt. If no other platforms remain after | |
15 removal, delete the rebaselined test from the file. | |
16 | |
17 At the end, the script generates a html that compares old and new baselines. | |
18 """ | |
19 | |
20 import logging | |
21 import optparse | |
22 import os | |
23 import re | |
24 import shutil | |
25 import subprocess | |
26 import sys | |
27 import tempfile | |
28 import time | |
29 import urllib | |
30 import webbrowser | |
31 import zipfile | |
32 | |
33 from layout_package import path_utils | |
34 from layout_package import test_expectations | |
35 from test_types import image_diff | |
36 from test_types import text_diff | |
37 | |
38 # Repository type constants. | |
39 REPO_SVN, REPO_UNKNOWN = range(2) | |
40 | |
41 BASELINE_SUFFIXES = ['.txt', '.png', '.checksum'] | |
42 REBASELINE_PLATFORM_ORDER = ['mac', 'win', 'win-xp', 'win-vista', 'linux'] | |
43 ARCHIVE_DIR_NAME_DICT = {'win': 'webkit-rel', | |
44 'win-vista': 'webkit-dbg-vista', | |
45 'win-xp': 'webkit-rel', | |
46 'mac': 'webkit-rel-mac5', | |
47 'linux': 'webkit-rel-linux', | |
48 'win-canary': 'webkit-rel-webkit-org', | |
49 'win-vista-canary': 'webkit-dbg-vista', | |
50 'win-xp-canary': 'webkit-rel-webkit-org', | |
51 'mac-canary': 'webkit-rel-mac-webkit-org', | |
52 'linux-canary': 'webkit-rel-linux-webkit-org'} | |
53 | |
54 | |
55 def RunShellWithReturnCode(command, print_output=False): | |
56 """Executes a command and returns the output and process return code. | |
57 | |
58 Args: | |
59 command: program and arguments. | |
60 print_output: if true, print the command results to standard output. | |
61 | |
62 Returns: | |
63 command output, return code | |
64 """ | |
65 | |
66 # Use a shell for subcommands on Windows to get a PATH search. | |
67 use_shell = sys.platform.startswith('win') | |
68 p = subprocess.Popen(command, stdout=subprocess.PIPE, | |
69 stderr=subprocess.STDOUT, shell=use_shell) | |
70 if print_output: | |
71 output_array = [] | |
72 while True: | |
73 line = p.stdout.readline() | |
74 if not line: | |
75 break | |
76 if print_output: | |
77 print line.strip('\n') | |
78 output_array.append(line) | |
79 output = ''.join(output_array) | |
80 else: | |
81 output = p.stdout.read() | |
82 p.wait() | |
83 p.stdout.close() | |
84 | |
85 return output, p.returncode | |
86 | |
87 | |
88 def RunShell(command, print_output=False): | |
89 """Executes a command and returns the output. | |
90 | |
91 Args: | |
92 command: program and arguments. | |
93 print_output: if true, print the command results to standard output. | |
94 | |
95 Returns: | |
96 command output | |
97 """ | |
98 | |
99 output, return_code = RunShellWithReturnCode(command, print_output) | |
100 return output | |
101 | |
102 | |
103 def LogDashedString(text, platform, logging_level=logging.INFO): | |
104 """Log text message with dashes on both sides.""" | |
105 | |
106 msg = text | |
107 if platform: | |
108 msg += ': ' + platform | |
109 if len(msg) < 78: | |
110 dashes = '-' * ((78 - len(msg)) / 2) | |
111 msg = '%s %s %s' % (dashes, msg, dashes) | |
112 | |
113 if logging_level == logging.ERROR: | |
114 logging.error(msg) | |
115 elif logging_level == logging.WARNING: | |
116 logging.warn(msg) | |
117 else: | |
118 logging.info(msg) | |
119 | |
120 | |
121 def SetupHtmlDirectory(html_directory): | |
122 """Setup the directory to store html results. | |
123 | |
124 All html related files are stored in the "rebaseline_html" subdirectory. | |
125 | |
126 Args: | |
127 html_directory: parent directory that stores the rebaselining results. | |
128 If None, a temp directory is created. | |
129 | |
130 Returns: | |
131 the directory that stores the html related rebaselining results. | |
132 """ | |
133 | |
134 if not html_directory: | |
135 html_directory = tempfile.mkdtemp() | |
136 elif not os.path.exists(html_directory): | |
137 os.mkdir(html_directory) | |
138 | |
139 html_directory = os.path.join(html_directory, 'rebaseline_html') | |
140 logging.info('Html directory: "%s"', html_directory) | |
141 | |
142 if os.path.exists(html_directory): | |
143 shutil.rmtree(html_directory, True) | |
144 logging.info('Deleted file at html directory: "%s"', html_directory) | |
145 | |
146 if not os.path.exists(html_directory): | |
147 os.mkdir(html_directory) | |
148 return html_directory | |
149 | |
150 | |
151 def GetResultFileFullpath(html_directory, baseline_filename, platform, | |
152 result_type): | |
153 """Get full path of the baseline result file. | |
154 | |
155 Args: | |
156 html_directory: directory that stores the html related files. | |
157 baseline_filename: name of the baseline file. | |
158 platform: win, linux or mac | |
159 result_type: type of the baseline result: '.txt', '.png'. | |
160 | |
161 Returns: | |
162 Full path of the baseline file for rebaselining result comparison. | |
163 """ | |
164 | |
165 base, ext = os.path.splitext(baseline_filename) | |
166 result_filename = '%s-%s-%s%s' % (base, platform, result_type, ext) | |
167 fullpath = os.path.join(html_directory, result_filename) | |
168 logging.debug(' Result file full path: "%s".', fullpath) | |
169 return fullpath | |
170 | |
171 | |
172 class Rebaseliner(object): | |
173 """Class to produce new baselines for a given platform.""" | |
174 | |
175 REVISION_REGEX = r'<a href=\"(\d+)/\">' | |
176 | |
177 def __init__(self, platform, options): | |
178 self._file_dir = path_utils.GetAbsolutePath( | |
179 os.path.dirname(sys.argv[0])) | |
180 self._platform = platform | |
181 self._options = options | |
182 self._rebaselining_tests = [] | |
183 self._rebaselined_tests = [] | |
184 | |
185 # Create tests and expectations helper which is used to: | |
186 # -. compile list of tests that need rebaselining. | |
187 # -. update the tests in test_expectations file after rebaseline | |
188 # is done. | |
189 self._test_expectations = \ | |
190 test_expectations.TestExpectations(None, | |
191 self._file_dir, | |
192 platform, | |
193 False, | |
194 False) | |
195 | |
196 self._repo_type = self._GetRepoType() | |
197 | |
198 def Run(self, backup): | |
199 """Run rebaseline process.""" | |
200 | |
201 LogDashedString('Compiling rebaselining tests', self._platform) | |
202 if not self._CompileRebaseliningTests(): | |
203 return True | |
204 | |
205 LogDashedString('Downloading archive', self._platform) | |
206 archive_file = self._DownloadBuildBotArchive() | |
207 logging.info('') | |
208 if not archive_file: | |
209 logging.error('No archive found.') | |
210 return False | |
211 | |
212 LogDashedString('Extracting and adding new baselines', self._platform) | |
213 if not self._ExtractAndAddNewBaselines(archive_file): | |
214 return False | |
215 | |
216 LogDashedString('Updating rebaselined tests in file', self._platform) | |
217 self._UpdateRebaselinedTestsInFile(backup) | |
218 logging.info('') | |
219 | |
220 if len(self._rebaselining_tests) != len(self._rebaselined_tests): | |
221 logging.warning('NOT ALL TESTS THAT NEED REBASELINING HAVE BEEN ' | |
222 'REBASELINED.') | |
223 logging.warning(' Total tests needing rebaselining: %d', | |
224 len(self._rebaselining_tests)) | |
225 logging.warning(' Total tests rebaselined: %d', | |
226 len(self._rebaselined_tests)) | |
227 return False | |
228 | |
229 logging.warning('All tests needing rebaselining were successfully ' | |
230 'rebaselined.') | |
231 | |
232 return True | |
233 | |
234 def GetRebaseliningTests(self): | |
235 return self._rebaselining_tests | |
236 | |
237 def _GetRepoType(self): | |
238 """Get the repository type that client is using.""" | |
239 | |
240 output, return_code = RunShellWithReturnCode(['svn', 'info'], False) | |
241 if return_code == 0: | |
242 return REPO_SVN | |
243 | |
244 return REPO_UNKNOWN | |
245 | |
246 def _CompileRebaseliningTests(self): | |
247 """Compile list of tests that need rebaselining for the platform. | |
248 | |
249 Returns: | |
250 List of tests that need rebaselining or | |
251 None if there is no such test. | |
252 """ | |
253 | |
254 self._rebaselining_tests = \ | |
255 self._test_expectations.GetRebaseliningFailures() | |
256 if not self._rebaselining_tests: | |
257 logging.warn('No tests found that need rebaselining.') | |
258 return None | |
259 | |
260 logging.info('Total number of tests needing rebaselining ' | |
261 'for "%s": "%d"', self._platform, | |
262 len(self._rebaselining_tests)) | |
263 | |
264 test_no = 1 | |
265 for test in self._rebaselining_tests: | |
266 logging.info(' %d: %s', test_no, test) | |
267 test_no += 1 | |
268 | |
269 return self._rebaselining_tests | |
270 | |
271 def _GetLatestRevision(self, url): | |
272 """Get the latest layout test revision number from buildbot. | |
273 | |
274 Args: | |
275 url: Url to retrieve layout test revision numbers. | |
276 | |
277 Returns: | |
278 latest revision or | |
279 None on failure. | |
280 """ | |
281 | |
282 logging.debug('Url to retrieve revision: "%s"', url) | |
283 | |
284 f = urllib.urlopen(url) | |
285 content = f.read() | |
286 f.close() | |
287 | |
288 revisions = re.findall(self.REVISION_REGEX, content) | |
289 if not revisions: | |
290 logging.error('Failed to find revision, content: "%s"', content) | |
291 return None | |
292 | |
293 revisions.sort(key=int) | |
294 logging.info('Latest revision: "%s"', revisions[len(revisions) - 1]) | |
295 return revisions[len(revisions) - 1] | |
296 | |
297 def _GetArchiveDirName(self, platform, webkit_canary): | |
298 """Get name of the layout test archive directory. | |
299 | |
300 Returns: | |
301 Directory name or | |
302 None on failure | |
303 """ | |
304 | |
305 if webkit_canary: | |
306 platform += '-canary' | |
307 | |
308 if platform in ARCHIVE_DIR_NAME_DICT: | |
309 return ARCHIVE_DIR_NAME_DICT[platform] | |
310 else: | |
311 logging.error('Cannot find platform key %s in archive ' | |
312 'directory name dictionary', platform) | |
313 return None | |
314 | |
315 def _GetArchiveUrl(self): | |
316 """Generate the url to download latest layout test archive. | |
317 | |
318 Returns: | |
319 Url to download archive or | |
320 None on failure | |
321 """ | |
322 | |
323 dir_name = self._GetArchiveDirName(self._platform, | |
324 self._options.webkit_canary) | |
325 if not dir_name: | |
326 return None | |
327 | |
328 logging.debug('Buildbot platform dir name: "%s"', dir_name) | |
329 | |
330 url_base = '%s/%s/' % (self._options.archive_url, dir_name) | |
331 latest_revision = self._GetLatestRevision(url_base) | |
332 if latest_revision is None or latest_revision <= 0: | |
333 return None | |
334 | |
335 archive_url = ('%s%s/layout-test-results.zip' % (url_base, | |
336 latest_revision)) | |
337 logging.info('Archive url: "%s"', archive_url) | |
338 return archive_url | |
339 | |
340 def _DownloadBuildBotArchive(self): | |
341 """Download layout test archive file from buildbot. | |
342 | |
343 Returns: | |
344 True if download succeeded or | |
345 False otherwise. | |
346 """ | |
347 | |
348 url = self._GetArchiveUrl() | |
349 if url is None: | |
350 return None | |
351 | |
352 fn = urllib.urlretrieve(url)[0] | |
353 logging.info('Archive downloaded and saved to file: "%s"', fn) | |
354 return fn | |
355 | |
356 def _ExtractAndAddNewBaselines(self, archive_file): | |
357 """Extract new baselines from archive and add them to SVN repository. | |
358 | |
359 Args: | |
360 archive_file: full path to the archive file. | |
361 | |
362 Returns: | |
363 List of tests that have been rebaselined or | |
364 None on failure. | |
365 """ | |
366 | |
367 zip_file = zipfile.ZipFile(archive_file, 'r') | |
368 zip_namelist = zip_file.namelist() | |
369 | |
370 logging.debug('zip file namelist:') | |
371 for name in zip_namelist: | |
372 logging.debug(' ' + name) | |
373 | |
374 platform = path_utils.PlatformName(self._platform) | |
375 logging.debug('Platform dir: "%s"', platform) | |
376 | |
377 test_no = 1 | |
378 self._rebaselined_tests = [] | |
379 for test in self._rebaselining_tests: | |
380 logging.info('Test %d: %s', test_no, test) | |
381 | |
382 found = False | |
383 svn_error = False | |
384 test_basename = os.path.splitext(test)[0] | |
385 for suffix in BASELINE_SUFFIXES: | |
386 archive_test_name = ('layout-test-results/%s-actual%s' % | |
387 (test_basename, suffix)) | |
388 logging.debug(' Archive test file name: "%s"', | |
389 archive_test_name) | |
390 if not archive_test_name in zip_namelist: | |
391 logging.info(' %s file not in archive.', suffix) | |
392 continue | |
393 | |
394 found = True | |
395 logging.info(' %s file found in archive.', suffix) | |
396 | |
397 # Extract new baseline from archive and save it to a temp file. | |
398 data = zip_file.read(archive_test_name) | |
399 temp_fd, temp_name = tempfile.mkstemp(suffix) | |
400 f = os.fdopen(temp_fd, 'wb') | |
401 f.write(data) | |
402 f.close() | |
403 | |
404 expected_filename = '%s-expected%s' % (test_basename, suffix) | |
405 expected_fullpath = os.path.join( | |
406 path_utils.ChromiumBaselinePath(platform), | |
407 expected_filename) | |
408 expected_fullpath = os.path.normpath(expected_fullpath) | |
409 logging.debug(' Expected file full path: "%s"', | |
410 expected_fullpath) | |
411 | |
412 # TODO(victorw): for now, the rebaselining tool checks whether | |
413 # or not THIS baseline is duplicate and should be skipped. | |
414 # We could improve the tool to check all baselines in upper | |
415 # and lower | |
416 # levels and remove all duplicated baselines. | |
417 if self._IsDupBaseline(temp_name, | |
418 expected_fullpath, | |
419 test, | |
420 suffix, | |
421 self._platform): | |
422 os.remove(temp_name) | |
423 self._DeleteBaseline(expected_fullpath) | |
424 continue | |
425 | |
426 # Create the new baseline directory if it doesn't already | |
427 # exist. | |
428 path_utils.MaybeMakeDirectory( | |
429 os.path.dirname(expected_fullpath)) | |
430 | |
431 shutil.move(temp_name, expected_fullpath) | |
432 | |
433 if not self._SvnAdd(expected_fullpath): | |
434 svn_error = True | |
435 elif suffix != '.checksum': | |
436 self._CreateHtmlBaselineFiles(expected_fullpath) | |
437 | |
438 if not found: | |
439 logging.warn(' No new baselines found in archive.') | |
440 else: | |
441 if svn_error: | |
442 logging.warn(' Failed to add baselines to SVN.') | |
443 else: | |
444 logging.info(' Rebaseline succeeded.') | |
445 self._rebaselined_tests.append(test) | |
446 | |
447 test_no += 1 | |
448 | |
449 zip_file.close() | |
450 os.remove(archive_file) | |
451 | |
452 return self._rebaselined_tests | |
453 | |
454 def _IsDupBaseline(self, new_baseline, baseline_path, test, suffix, | |
455 platform): | |
456 """Check whether a baseline is duplicate and can fallback to same | |
457 baseline for another platform. For example, if a test has same | |
458 baseline on linux and windows, then we only store windows | |
459 baseline and linux baseline will fallback to the windows version. | |
460 | |
461 Args: | |
462 expected_filename: baseline expectation file name. | |
463 test: test name. | |
464 suffix: file suffix of the expected results, including dot; | |
465 e.g. '.txt' or '.png'. | |
466 platform: baseline platform 'mac', 'win' or 'linux'. | |
467 | |
468 Returns: | |
469 True if the baseline is unnecessary. | |
470 False otherwise. | |
471 """ | |
472 test_filepath = os.path.join(path_utils.LayoutTestsDir(), test) | |
473 all_baselines = path_utils.ExpectedBaselines(test_filepath, | |
474 suffix, | |
475 platform, | |
476 True) | |
477 for (fallback_dir, fallback_file) in all_baselines: | |
478 if fallback_dir and fallback_file: | |
479 fallback_fullpath = os.path.normpath( | |
480 os.path.join(fallback_dir, fallback_file)) | |
481 if fallback_fullpath.lower() != baseline_path.lower(): | |
482 if not self._DiffBaselines(new_baseline, | |
483 fallback_fullpath): | |
484 logging.info(' Found same baseline at %s', | |
485 fallback_fullpath) | |
486 return True | |
487 else: | |
488 return False | |
489 | |
490 return False | |
491 | |
492 def _DiffBaselines(self, file1, file2): | |
493 """Check whether two baselines are different. | |
494 | |
495 Args: | |
496 file1, file2: full paths of the baselines to compare. | |
497 | |
498 Returns: | |
499 True if two files are different or have different extensions. | |
500 False otherwise. | |
501 """ | |
502 | |
503 ext1 = os.path.splitext(file1)[1].upper() | |
504 ext2 = os.path.splitext(file2)[1].upper() | |
505 if ext1 != ext2: | |
506 logging.warn('Files to compare have different ext. ' | |
507 'File1: %s; File2: %s', file1, file2) | |
508 return True | |
509 | |
510 if ext1 == '.PNG': | |
511 return image_diff.ImageDiff(self._platform, '').DiffFiles(file1, | |
512 file2) | |
513 else: | |
514 return text_diff.TestTextDiff(self._platform, '').DiffFiles(file1, | |
515 file2) | |
516 | |
517 def _DeleteBaseline(self, filename): | |
518 """Remove the file from repository and delete it from disk. | |
519 | |
520 Args: | |
521 filename: full path of the file to delete. | |
522 """ | |
523 | |
524 if not filename or not os.path.isfile(filename): | |
525 return | |
526 | |
527 if self._repo_type == REPO_SVN: | |
528 parent_dir, basename = os.path.split(filename) | |
529 original_dir = os.getcwd() | |
530 os.chdir(parent_dir) | |
531 RunShell(['svn', 'delete', '--force', basename], False) | |
532 os.chdir(original_dir) | |
533 else: | |
534 os.remove(filename) | |
535 | |
536 def _UpdateRebaselinedTestsInFile(self, backup): | |
537 """Update the rebaselined tests in test expectations file. | |
538 | |
539 Args: | |
540 backup: if True, backup the original test expectations file. | |
541 | |
542 Returns: | |
543 no | |
544 """ | |
545 | |
546 if self._rebaselined_tests: | |
547 self._test_expectations.RemovePlatformFromFile( | |
548 self._rebaselined_tests, self._platform, backup) | |
549 else: | |
550 logging.info('No test was rebaselined so nothing to remove.') | |
551 | |
552 def _SvnAdd(self, filename): | |
553 """Add the file to SVN repository. | |
554 | |
555 Args: | |
556 filename: full path of the file to add. | |
557 | |
558 Returns: | |
559 True if the file already exists in SVN or is sucessfully added | |
560 to SVN. | |
561 False otherwise. | |
562 """ | |
563 | |
564 if not filename: | |
565 return False | |
566 | |
567 parent_dir, basename = os.path.split(filename) | |
568 if self._repo_type != REPO_SVN or parent_dir == filename: | |
569 logging.info("No svn checkout found, skip svn add.") | |
570 return True | |
571 | |
572 original_dir = os.getcwd() | |
573 os.chdir(parent_dir) | |
574 status_output = RunShell(['svn', 'status', basename], False) | |
575 os.chdir(original_dir) | |
576 output = status_output.upper() | |
577 if output.startswith('A') or output.startswith('M'): | |
578 logging.info(' File already added to SVN: "%s"', filename) | |
579 return True | |
580 | |
581 if output.find('IS NOT A WORKING COPY') >= 0: | |
582 logging.info(' File is not a working copy, add its parent: "%s"', | |
583 parent_dir) | |
584 return self._SvnAdd(parent_dir) | |
585 | |
586 os.chdir(parent_dir) | |
587 add_output = RunShell(['svn', 'add', basename], True) | |
588 os.chdir(original_dir) | |
589 output = add_output.upper().rstrip() | |
590 if output.startswith('A') and output.find(basename.upper()) >= 0: | |
591 logging.info(' Added new file: "%s"', filename) | |
592 self._SvnPropSet(filename) | |
593 return True | |
594 | |
595 if (not status_output) and (add_output.upper().find( | |
596 'ALREADY UNDER VERSION CONTROL') >= 0): | |
597 logging.info(' File already under SVN and has no change: "%s"', | |
598 filename) | |
599 return True | |
600 | |
601 logging.warn(' Failed to add file to SVN: "%s"', filename) | |
602 logging.warn(' Svn status output: "%s"', status_output) | |
603 logging.warn(' Svn add output: "%s"', add_output) | |
604 return False | |
605 | |
606 def _SvnPropSet(self, filename): | |
607 """Set the baseline property | |
608 | |
609 Args: | |
610 filename: full path of the file to add. | |
611 | |
612 Returns: | |
613 True if the file already exists in SVN or is sucessfully added | |
614 to SVN. | |
615 False otherwise. | |
616 """ | |
617 ext = os.path.splitext(filename)[1].upper() | |
618 if ext != '.TXT' and ext != '.PNG' and ext != '.CHECKSUM': | |
619 return | |
620 | |
621 parent_dir, basename = os.path.split(filename) | |
622 original_dir = os.getcwd() | |
623 os.chdir(parent_dir) | |
624 if ext == '.PNG': | |
625 cmd = ['svn', 'pset', 'svn:mime-type', 'image/png', basename] | |
626 else: | |
627 cmd = ['svn', 'pset', 'svn:eol-style', 'LF', basename] | |
628 | |
629 logging.debug(' Set svn prop: %s', ' '.join(cmd)) | |
630 RunShell(cmd, False) | |
631 os.chdir(original_dir) | |
632 | |
633 def _CreateHtmlBaselineFiles(self, baseline_fullpath): | |
634 """Create baseline files (old, new and diff) in html directory. | |
635 | |
636 The files are used to compare the rebaselining results. | |
637 | |
638 Args: | |
639 baseline_fullpath: full path of the expected baseline file. | |
640 """ | |
641 | |
642 if not baseline_fullpath or not os.path.exists(baseline_fullpath): | |
643 return | |
644 | |
645 # Copy the new baseline to html directory for result comparison. | |
646 baseline_filename = os.path.basename(baseline_fullpath) | |
647 new_file = GetResultFileFullpath(self._options.html_directory, | |
648 baseline_filename, | |
649 self._platform, | |
650 'new') | |
651 shutil.copyfile(baseline_fullpath, new_file) | |
652 logging.info(' Html: copied new baseline file from "%s" to "%s".', | |
653 baseline_fullpath, new_file) | |
654 | |
655 # Get the old baseline from SVN and save to the html directory. | |
656 output = RunShell(['svn', 'cat', '-r', 'BASE', baseline_fullpath]) | |
657 if (not output) or (output.upper().rstrip().endswith( | |
658 'NO SUCH FILE OR DIRECTORY')): | |
659 logging.info(' No base file: "%s"', baseline_fullpath) | |
660 return | |
661 base_file = GetResultFileFullpath(self._options.html_directory, | |
662 baseline_filename, | |
663 self._platform, | |
664 'old') | |
665 f = open(base_file, 'wb') | |
666 f.write(output) | |
667 f.close() | |
668 logging.info(' Html: created old baseline file: "%s".', | |
669 base_file) | |
670 | |
671 # Get the diff between old and new baselines and save to the html dir. | |
672 if baseline_filename.upper().endswith('.TXT'): | |
673 # If the user specified a custom diff command in their svn config | |
674 # file, then it'll be used when we do svn diff, which we don't want | |
675 # to happen since we want the unified diff. Using --diff-cmd=diff | |
676 # doesn't always work, since they can have another diff executable | |
677 # in their path that gives different line endings. So we use a | |
678 # bogus temp directory as the config directory, which gets | |
679 # around these problems. | |
680 if sys.platform.startswith("win"): | |
681 parent_dir = tempfile.gettempdir() | |
682 else: | |
683 parent_dir = sys.path[0] # tempdir is not secure. | |
684 bogus_dir = os.path.join(parent_dir, "temp_svn_config") | |
685 logging.debug(' Html: temp config dir: "%s".', bogus_dir) | |
686 if not os.path.exists(bogus_dir): | |
687 os.mkdir(bogus_dir) | |
688 delete_bogus_dir = True | |
689 else: | |
690 delete_bogus_dir = False | |
691 | |
692 output = RunShell(["svn", "diff", "--config-dir", bogus_dir, | |
693 baseline_fullpath]) | |
694 if output: | |
695 diff_file = GetResultFileFullpath(self._options.html_directory, | |
696 baseline_filename, | |
697 self._platform, | |
698 'diff') | |
699 f = open(diff_file, 'wb') | |
700 f.write(output) | |
701 f.close() | |
702 logging.info(' Html: created baseline diff file: "%s".', | |
703 diff_file) | |
704 | |
705 if delete_bogus_dir: | |
706 shutil.rmtree(bogus_dir, True) | |
707 logging.debug(' Html: removed temp config dir: "%s".', | |
708 bogus_dir) | |
709 | |
710 | |
711 class HtmlGenerator(object): | |
712 """Class to generate rebaselining result comparison html.""" | |
713 | |
714 HTML_REBASELINE = ('<html>' | |
715 '<head>' | |
716 '<style>' | |
717 'body {font-family: sans-serif;}' | |
718 '.mainTable {background: #666666;}' | |
719 '.mainTable td , .mainTable th {background: white;}' | |
720 '.detail {margin-left: 10px; margin-top: 3px;}' | |
721 '</style>' | |
722 '<title>Rebaselining Result Comparison (%(time)s)' | |
723 '</title>' | |
724 '</head>' | |
725 '<body>' | |
726 '<h2>Rebaselining Result Comparison (%(time)s)</h2>' | |
727 '%(body)s' | |
728 '</body>' | |
729 '</html>') | |
730 HTML_NO_REBASELINING_TESTS = ( | |
731 '<p>No tests found that need rebaselining.</p>') | |
732 HTML_TABLE_TEST = ('<table class="mainTable" cellspacing=1 cellpadding=5>' | |
733 '%s</table><br>') | |
734 HTML_TR_TEST = ('<tr>' | |
735 '<th style="background-color: #CDECDE; border-bottom: ' | |
736 '1px solid black; font-size: 18pt; font-weight: bold" ' | |
737 'colspan="5">' | |
738 '<a href="%s">%s</a>' | |
739 '</th>' | |
740 '</tr>') | |
741 HTML_TEST_DETAIL = ('<div class="detail">' | |
742 '<tr>' | |
743 '<th width="100">Baseline</th>' | |
744 '<th width="100">Platform</th>' | |
745 '<th width="200">Old</th>' | |
746 '<th width="200">New</th>' | |
747 '<th width="150">Difference</th>' | |
748 '</tr>' | |
749 '%s' | |
750 '</div>') | |
751 HTML_TD_NOLINK = '<td align=center><a>%s</a></td>' | |
752 HTML_TD_LINK = '<td align=center><a href="%(uri)s">%(name)s</a></td>' | |
753 HTML_TD_LINK_IMG = ('<td><a href="%(uri)s">' | |
754 '<img style="width: 200" src="%(uri)s" /></a></td>') | |
755 HTML_TR = '<tr>%s</tr>' | |
756 | |
757 def __init__(self, options, platforms, rebaselining_tests): | |
758 self._html_directory = options.html_directory | |
759 self._platforms = platforms | |
760 self._rebaselining_tests = rebaselining_tests | |
761 self._html_file = os.path.join(options.html_directory, | |
762 'rebaseline.html') | |
763 | |
764 def GenerateHtml(self): | |
765 """Generate html file for rebaselining result comparison.""" | |
766 | |
767 logging.info('Generating html file') | |
768 | |
769 html_body = '' | |
770 if not self._rebaselining_tests: | |
771 html_body += self.HTML_NO_REBASELINING_TESTS | |
772 else: | |
773 tests = list(self._rebaselining_tests) | |
774 tests.sort() | |
775 | |
776 test_no = 1 | |
777 for test in tests: | |
778 logging.info('Test %d: %s', test_no, test) | |
779 html_body += self._GenerateHtmlForOneTest(test) | |
780 | |
781 html = self.HTML_REBASELINE % ({'time': time.asctime(), | |
782 'body': html_body}) | |
783 logging.debug(html) | |
784 | |
785 f = open(self._html_file, 'w') | |
786 f.write(html) | |
787 f.close() | |
788 | |
789 logging.info('Baseline comparison html generated at "%s"', | |
790 self._html_file) | |
791 | |
792 def ShowHtml(self): | |
793 """Launch the rebaselining html in brwoser.""" | |
794 | |
795 logging.info('Launching html: "%s"', self._html_file) | |
796 | |
797 html_uri = path_utils.FilenameToUri(self._html_file) | |
798 webbrowser.open(html_uri, 1) | |
799 | |
800 logging.info('Html launched.') | |
801 | |
802 def _GenerateBaselineLinks(self, test_basename, suffix, platform): | |
803 """Generate links for baseline results (old, new and diff). | |
804 | |
805 Args: | |
806 test_basename: base filename of the test | |
807 suffix: baseline file suffixes: '.txt', '.png' | |
808 platform: win, linux or mac | |
809 | |
810 Returns: | |
811 html links for showing baseline results (old, new and diff) | |
812 """ | |
813 | |
814 baseline_filename = '%s-expected%s' % (test_basename, suffix) | |
815 logging.debug(' baseline filename: "%s"', baseline_filename) | |
816 | |
817 new_file = GetResultFileFullpath(self._html_directory, | |
818 baseline_filename, | |
819 platform, | |
820 'new') | |
821 logging.info(' New baseline file: "%s"', new_file) | |
822 if not os.path.exists(new_file): | |
823 logging.info(' No new baseline file: "%s"', new_file) | |
824 return '' | |
825 | |
826 old_file = GetResultFileFullpath(self._html_directory, | |
827 baseline_filename, | |
828 platform, | |
829 'old') | |
830 logging.info(' Old baseline file: "%s"', old_file) | |
831 if suffix == '.png': | |
832 html_td_link = self.HTML_TD_LINK_IMG | |
833 else: | |
834 html_td_link = self.HTML_TD_LINK | |
835 | |
836 links = '' | |
837 if os.path.exists(old_file): | |
838 links += html_td_link % {'uri': path_utils.FilenameToUri(old_file), | |
839 'name': baseline_filename} | |
840 else: | |
841 logging.info(' No old baseline file: "%s"', old_file) | |
842 links += self.HTML_TD_NOLINK % '' | |
843 | |
844 links += html_td_link % {'uri': path_utils.FilenameToUri(new_file), | |
845 'name': baseline_filename} | |
846 | |
847 diff_file = GetResultFileFullpath(self._html_directory, | |
848 baseline_filename, | |
849 platform, | |
850 'diff') | |
851 logging.info(' Baseline diff file: "%s"', diff_file) | |
852 if os.path.exists(diff_file): | |
853 links += html_td_link % {'uri': path_utils.FilenameToUri( | |
854 diff_file), 'name': 'Diff'} | |
855 else: | |
856 logging.info(' No baseline diff file: "%s"', diff_file) | |
857 links += self.HTML_TD_NOLINK % '' | |
858 | |
859 return links | |
860 | |
861 def _GenerateHtmlForOneTest(self, test): | |
862 """Generate html for one rebaselining test. | |
863 | |
864 Args: | |
865 test: layout test name | |
866 | |
867 Returns: | |
868 html that compares baseline results for the test. | |
869 """ | |
870 | |
871 test_basename = os.path.basename(os.path.splitext(test)[0]) | |
872 logging.info(' basename: "%s"', test_basename) | |
873 rows = [] | |
874 for suffix in BASELINE_SUFFIXES: | |
875 if suffix == '.checksum': | |
876 continue | |
877 | |
878 logging.info(' Checking %s files', suffix) | |
879 for platform in self._platforms: | |
880 links = self._GenerateBaselineLinks(test_basename, suffix, | |
881 platform) | |
882 if links: | |
883 row = self.HTML_TD_NOLINK % self._GetBaselineResultType( | |
884 suffix) | |
885 row += self.HTML_TD_NOLINK % platform | |
886 row += links | |
887 logging.debug(' html row: %s', row) | |
888 | |
889 rows.append(self.HTML_TR % row) | |
890 | |
891 if rows: | |
892 test_path = os.path.join(path_utils.LayoutTestsDir(), test) | |
893 html = self.HTML_TR_TEST % (path_utils.FilenameToUri(test_path), | |
894 test) | |
895 html += self.HTML_TEST_DETAIL % ' '.join(rows) | |
896 | |
897 logging.debug(' html for test: %s', html) | |
898 return self.HTML_TABLE_TEST % html | |
899 | |
900 return '' | |
901 | |
902 def _GetBaselineResultType(self, suffix): | |
903 """Name of the baseline result type.""" | |
904 | |
905 if suffix == '.png': | |
906 return 'Pixel' | |
907 elif suffix == '.txt': | |
908 return 'Render Tree' | |
909 else: | |
910 return 'Other' | |
911 | |
912 | |
913 def main(): | |
914 """Main function to produce new baselines.""" | |
915 | |
916 option_parser = optparse.OptionParser() | |
917 option_parser.add_option('-v', '--verbose', | |
918 action='store_true', | |
919 default=False, | |
920 help='include debug-level logging.') | |
921 | |
922 option_parser.add_option('-p', '--platforms', | |
923 default='mac,win,win-xp,win-vista,linux', | |
924 help=('Comma delimited list of platforms ' | |
925 'that need rebaselining.')) | |
926 | |
927 option_parser.add_option('-u', '--archive_url', | |
928 default=('http://build.chromium.org/buildbot/' | |
929 'layout_test_results'), | |
930 help=('Url to find the layout test result archive' | |
931 ' file.')) | |
932 | |
933 option_parser.add_option('-w', '--webkit_canary', | |
934 action='store_true', | |
935 default=False, | |
936 help=('If True, pull baselines from webkit.org ' | |
937 'canary bot.')) | |
938 | |
939 option_parser.add_option('-b', '--backup', | |
940 action='store_true', | |
941 default=False, | |
942 help=('Whether or not to backup the original test' | |
943 ' expectations file after rebaseline.')) | |
944 | |
945 option_parser.add_option('-d', '--html_directory', | |
946 default='', | |
947 help=('The directory that stores the results for' | |
948 ' rebaselining comparison.')) | |
949 | |
950 options = option_parser.parse_args()[0] | |
951 | |
952 # Set up our logging format. | |
953 log_level = logging.INFO | |
954 if options.verbose: | |
955 log_level = logging.DEBUG | |
956 logging.basicConfig(level=log_level, | |
957 format=('%(asctime)s %(filename)s:%(lineno)-3d ' | |
958 '%(levelname)s %(message)s'), | |
959 datefmt='%y%m%d %H:%M:%S') | |
960 | |
961 # Verify 'platforms' option is valid | |
962 if not options.platforms: | |
963 logging.error('Invalid "platforms" option. --platforms must be ' | |
964 'specified in order to rebaseline.') | |
965 sys.exit(1) | |
966 platforms = [p.strip().lower() for p in options.platforms.split(',')] | |
967 for platform in platforms: | |
968 if not platform in REBASELINE_PLATFORM_ORDER: | |
969 logging.error('Invalid platform: "%s"' % (platform)) | |
970 sys.exit(1) | |
971 | |
972 # Adjust the platform order so rebaseline tool is running at the order of | |
973 # 'mac', 'win' and 'linux'. This is in same order with layout test baseline | |
974 # search paths. It simplifies how the rebaseline tool detects duplicate | |
975 # baselines. Check _IsDupBaseline method for details. | |
976 rebaseline_platforms = [] | |
977 for platform in REBASELINE_PLATFORM_ORDER: | |
978 if platform in platforms: | |
979 rebaseline_platforms.append(platform) | |
980 | |
981 options.html_directory = SetupHtmlDirectory(options.html_directory) | |
982 | |
983 rebaselining_tests = set() | |
984 backup = options.backup | |
985 for platform in rebaseline_platforms: | |
986 rebaseliner = Rebaseliner(platform, options) | |
987 | |
988 logging.info('') | |
989 LogDashedString('Rebaseline started', platform) | |
990 if rebaseliner.Run(backup): | |
991 # Only need to backup one original copy of test expectation file. | |
992 backup = False | |
993 LogDashedString('Rebaseline done', platform) | |
994 else: | |
995 LogDashedString('Rebaseline failed', platform, logging.ERROR) | |
996 | |
997 rebaselining_tests |= set(rebaseliner.GetRebaseliningTests()) | |
998 | |
999 logging.info('') | |
1000 LogDashedString('Rebaselining result comparison started', None) | |
1001 html_generator = HtmlGenerator(options, | |
1002 rebaseline_platforms, | |
1003 rebaselining_tests) | |
1004 html_generator.GenerateHtml() | |
1005 html_generator.ShowHtml() | |
1006 LogDashedString('Rebaselining result comparison done', None) | |
1007 | |
1008 sys.exit(0) | |
1009 | |
1010 if '__main__' == __name__: | |
1011 main() | |
OLD | NEW |