| OLD | NEW |
| 1 #!/usr/bin/python | 1 #!/usr/bin/python |
| 2 # Copyright (c) 2011 The Chromium Authors. All rights reserved. | 2 # Copyright (c) 2011 The Chromium Authors. All rights reserved. |
| 3 # Use of this source code is governed by a BSD-style license that can be | 3 # Use of this source code is governed by a BSD-style license that can be |
| 4 # found in the LICENSE file. | 4 # found in the LICENSE file. |
| 5 | 5 |
| 6 """A module for TestExpectionsManager. | 6 """A Module to analyze test expectations for Webkit layout tests.""" |
| 7 | 7 |
| 8 TestExpectaionManager manages data for Webkit layout tests. | 8 import re |
| 9 """ | 9 import urllib2 |
| 10 | 10 |
| 11 import csv | 11 # Default Webkit SVN location for chromium test expectation file. |
| 12 import re | 12 # TODO(imasaki): support multiple test expectations files. |
| 13 import time | 13 DEFAULT_TEST_EXPECTATION_LOCATION = ( |
| 14 import urllib | 14 'http://svn.webkit.org/repository/webkit/trunk/' |
| 15 import pysvn | 15 'LayoutTests/platform/chromium/test_expectations.txt') |
| 16 | 16 |
| 17 from csv_utils import CsvUtils | 17 # The following is from test expectation syntax. The detail can be found in |
| 18 from layout_test_test_case import LayoutTestCaseManager | 18 # http://www.chromium.org/developers/testing/ |
| 19 from test_case_patterns import TestCasePatterns | 19 # webkit-layout-tests#TOC-Test-Expectations |
| 20 # <decision> ::== [SKIP] [WONTFIX] [SLOW] |
| 21 DECISION_NAMES = ['SKIP', 'WONTFIX', 'SLOW'] |
| 22 # <platform> ::== [GPU] [CPU] [WIN] [LINUX] [MAC] |
| 23 PLATFORM_NAMES = ['GPU', 'CPU', 'WIN', 'LINUX', 'MAC'] |
| 24 # <config> ::== RELEASE | DEBUG |
| 25 CONFIG_NAMES = ['RELEASE', 'DEBUG'] |
| 26 # <EXPECTATION_NAMES> ::== \ |
| 27 # [FAIL] [PASS] [CRASH] [TIMEOUT] [IMAGE] [TEXT] [IMAGE+TEXT] |
| 28 EXPECTATION_NAMES = ['FAIL', 'PASS', 'CRASH', |
| 29 'TIMEOUT', 'IMAGE', 'TEXT', |
| 30 'IMAGE+TEXT'] |
| 31 ALL_TE_KEYWORDS = (DECISION_NAMES + PLATFORM_NAMES + CONFIG_NAMES + |
| 32 EXPECTATION_NAMES) |
| 20 | 33 |
| 21 | 34 |
| 22 class TestExpectationsManager(object): | 35 class TestExpectations(object): |
| 23 """This class manages test expectations data for Webkit layout tests. | 36 """A class to model the content of test expectation file for analysis. |
| 24 | 37 |
| 25 The detail of the test expectation file can be found in | 38 The raw test expectation file can be found in |
| 26 http://trac.webkit.org/wiki/TestExpectations. | 39 |DEFAULT_TEST_EXPECTATION_LOCATION|. |
| 40 It is necessary to parse this file and store meaningful information for |
| 41 the analysis (joining with existing layout tests using a test name). |
| 42 Instance variable |all_test_expectation_info| is used. |
| 43 A test name such as 'media/video-source-type.html' is used for the key |
| 44 to store information. However, a test name can appear multiple times in |
| 45 the test expectation file. So, the map should keep all the occurrence |
| 46 information. For example, the current test expectation file has the following |
| 47 two entries: |
| 48 BUGWK58587 LINUX DEBUG GPU : media/video-zoom.html = IMAGE |
| 49 BUGCR86714 MAC GPU : media/video-zoom.html = CRASH IMAGE |
| 50 In this case, all_test_expectation_info['media/video-zoom.html'] will have |
| 51 a list with two elements, each of which is the map of the test expectation |
| 52 information. |
| 53 """ |
| 27 | 54 |
| 28 This class does the following: | 55 def __init__(self, url=DEFAULT_TEST_EXPECTATION_LOCATION): |
| 29 (1) get test expectation file from WebKit subversion source | 56 """Read the test expectation file from the specified URL and parse it. |
| 30 repository using pysvn. | 57 |
| 31 (2) parse the file and generate CSV entries. | 58 All parsed information is stored into instance variable |
| 32 (3) create hyperlinks where appropriate (e.g., test case name). | 59 |all_test_expectation_info|, which is a dictionary mapping a string test |
| 60 name to a list of dictionaries containing test expectation entry |
| 61 information. An example of such dictionary: |
| 62 {'media/video-zoom.html': [{'LINUX': True, 'DEBUG': True ....}, |
| 63 {'MAC': True, 'GPU': True ....}] |
| 64 which is produced from the lines: |
| 65 BUGCR86714 MAC GPU : media/video-zoom.html = CRASH IMAGE |
| 66 BUGCR86714 LINUX DEBUG : media/video-zoom.html = IMAGE |
| 67 |
| 68 Args: |
| 69 url: A URL string for the test expectation file. |
| 70 |
| 71 Raises: |
| 72 NameError when the test expectation file cannot be retrieved from |url|. |
| 33 """ | 73 """ |
| 74 self.all_test_expectation_info = {} |
| 75 resp = urllib2.urlopen(url) |
| 76 if resp.code != 200: |
| 77 raise NameError('Test expectation file does not exist in %s' % source) |
| 78 # Start parsing each line. |
| 79 comments = '' |
| 80 for line in resp.read().split('\n'): |
| 81 if line.startswith('//'): |
| 82 # Comments can be multiple lines. |
| 83 comments += line.replace('//', '') |
| 84 elif not line: |
| 85 comments = '' |
| 86 else: |
| 87 test_expectation_info = self.ParseLine(line, comments) |
| 88 testname = TestExpectations.ExtractTestOrDirectoryName(line) |
| 89 if not testname in self.all_test_expectation_info: |
| 90 self.all_test_expectation_info[testname] = [] |
| 91 # This is a list for multiple entries. |
| 92 self.all_test_expectation_info[testname].append(test_expectation_info) |
| 34 | 93 |
| 35 DEFAULT_TEST_EXPECTATION_DIR = ( | 94 @staticmethod |
| 36 'http://svn.webkit.org/repository/webkit/trunk/' | 95 def ExtractTestOrDirectoryName(line): |
| 37 'LayoutTests/platform/chromium/') | 96 """Extract either a test name or a directory name from each line. |
| 38 | 97 |
| 39 DEFAULT_TEST_EXPECTATION_LOCATION = ( | 98 Please note the name in the test expectation entry can be test name or |
| 40 'http://svn.webkit.org/repository/webkit/trunk/' | 99 directory: Such examples are: |
| 41 'LayoutTests/platform/chromium/test_expectations.txt') | 100 BUGWK43668 SKIP : media/track/ = TIMEOUT |
| 42 | 101 |
| 43 CHROME_BUG_URL = 'http://code.google.com/p/chromium/issues/detail?id=' | 102 Args: |
| 103 line: a line in the test expectation file. |
| 44 | 104 |
| 45 WEBKIT_BUG_URL = 'https://bugs.webkit.org/show_bug.cgi?id=' | 105 Returns: |
| 106 a test name or directory name string. Returns '' if no match. |
| 46 | 107 |
| 47 FLAKINESS_DASHBOARD_LINK = ( | 108 Raises: |
| 48 'http://test-results.appspot.com/dashboards/' | 109 ValueError when there is no test name match. |
| 49 'flakiness_dashboard.html#tests=') | 110 """ |
| 111 # First try to find test name ending with .html. |
| 112 matches = re.search(r':\s+(\S+.html)', line) |
| 113 # Next try to find directory name. |
| 114 if matches: |
| 115 return matches.group(1) |
| 116 matches = re.search(r':\s+(\S+)', line) |
| 117 if matches: |
| 118 return matches.group(1) |
| 119 else: |
| 120 raise ValueError('test or dictionary name cannot be found in the line') |
| 50 | 121 |
| 51 TEST_CASE_PATTERNS = TestCasePatterns() | 122 @staticmethod |
| 123 def ParseLine(line, comment_prefix): |
| 124 """Parse each line in test expectation and update test expectation info. |
| 52 | 125 |
| 53 OTHER_FIELD_NAMES = ['TestCase', 'Media', 'Flaky', 'WebKitD', 'Bug', | 126 This function checks for each entry from |ALL_TE_KEYWORDS| in the current |
| 54 'Test'] | 127 line and stores it in the test expectation info map if found. Comment |
| 128 and bug information is also stored in the map. |
| 55 | 129 |
| 56 # The following is from test expectation syntax. | 130 Args: |
| 57 # BUG[0-9]+ [SKIP] [WONTFIX] [SLOW] | 131 line: a line in the test expectation file. For example, |
| 58 DECISION_COLUMN_NAMES = ['SKIP', 'SLOW'] | 132 "BUGCR86714 MAC GPU : media/video-zoom.html = CRASH IMAGE" |
| 59 # <platform> ::== [GPU] [CPU] [WIN] [LINUX] [MAC] | 133 comment_prefix: comments from the test expectation file occurring just |
| 60 PLATFORM_COLUMN_NAMES = ['GPU', 'CPU', 'WIN', 'LINUX', 'MAC'] | 134 before the current line being parsed. |
| 61 #<config> ::== RELEASE | DEBUG | |
| 62 CONFIG_COLUMN_NAMES = ['RELEASE', 'DEBUG'] | |
| 63 # <EXPECTATION_COLUMN_NAMES> ::== \ | |
| 64 # [FAIL] [PASS] [CRASH] [TIMEOUT] [IMAGE] [TEXT] [IMAGE+TEXT] | |
| 65 EXPECTATION_COLUMN_NAMES = ['FAIL', 'PASS', 'CRASH', | |
| 66 'TIMEOUT', 'IMAGE', 'TEXT', | |
| 67 'IMAGE+TEXT'] | |
| 68 | 135 |
| 69 # These are coming from metadata in comments in the test expectation file. | 136 Returns: |
| 70 COMMENT_COLUMN_NAMES = ['UNIMPLEMENTED', 'KNOWNISSUE', 'TESTISSUE', | 137 a dictionary containing test expectation info, including comment and bug |
| 71 'WONTFIX'] | 138 info. |
| 72 | 139 """ |
| 73 RAW_COMMENT_COLUMN_NAME = 'COMMENTS' | 140 test_expectation_info = {} |
| 74 | 141 # Store comments. |
| 75 def __init__(self): | 142 inline_comments = '' |
| 76 """Initialize the test cases.""" | 143 if '//' in line: |
| 77 self.testcases = [] | 144 inline_comments = line[line.rindex('//') + 2:] |
| 78 | 145 # Remove the inline comments to avoid the case where keywords are in |
| 79 def get_column_indexes(self, column_names): | 146 # inline comments. |
| 80 """Get column indexes for given column names. | 147 line = line[0:line.rindex('//')] |
| 81 | 148 for name in ALL_TE_KEYWORDS: |
| 82 Args: | 149 if name in line: |
| 83 column_names: a list of column names. | 150 test_expectation_info[name] = True |
| 84 | 151 test_expectation_info['Comments'] = comment_prefix + inline_comments |
| 85 Returns: | 152 # Store bug informations. |
| 86 a list of indexes for the given column names. | 153 bugs = re.findall(r'BUG\w+', line) |
| 87 """ | 154 if bugs: |
| 88 all_field_names = self.get_all_column_names(True, True) | 155 test_expectation_info['Bugs'] = bugs |
| 89 return [all_field_names.index( | 156 return test_expectation_info |
| 90 field_name) for field_name in column_names] | |
| 91 | |
| 92 def get_all_column_names(self, include_other_fields, include_comments): | |
| 93 """Get all column names that are used in CSV file. | |
| 94 | |
| 95 Args: | |
| 96 include_other_fields: a boolean that indicates the result should | |
| 97 include other column names (defined in OTHER_COLUMNS_NAMES). | |
| 98 include_comments: a boolean that indicates the result should | |
| 99 include comment-related names. | |
| 100 | |
| 101 Returns: | |
| 102 a list that contains column names. | |
| 103 """ | |
| 104 return_list = ( | |
| 105 self.DECISION_COLUMN_NAMES + self.PLATFORM_COLUMN_NAMES + | |
| 106 self.CONFIG_COLUMN_NAMES + self.EXPECTATION_COLUMN_NAMES) | |
| 107 if include_other_fields: | |
| 108 return_list = self.OTHER_FIELD_NAMES + return_list | |
| 109 if include_comments: | |
| 110 return_list.extend(self.COMMENT_COLUMN_NAMES) | |
| 111 return_list.append(self.RAW_COMMENT_COLUMN_NAME) | |
| 112 return return_list | |
| 113 | |
| 114 def get_test_case_element(self, test_case, column_names): | |
| 115 """Get test case elements. | |
| 116 | |
| 117 A test case is a collection of test case elements. | |
| 118 | |
| 119 Args: | |
| 120 test_case: test case data that contains all test case elements. | |
| 121 column_names: column names for specifying test case elements. | |
| 122 | |
| 123 Returns: | |
| 124 A list of test case elements for the given column names. | |
| 125 """ | |
| 126 field_indexes = self.get_column_indexes(column_names) | |
| 127 test_case = self.get_test_case_by_name(test_case) | |
| 128 if test_case is None: | |
| 129 return [] | |
| 130 return [test_case[fid] for fid in field_indexes] | |
| 131 | |
| 132 def get_test_case_by_name(self, target): | |
| 133 """Get test case object by test case name. | |
| 134 | |
| 135 Args: | |
| 136 target: test case name. | |
| 137 | |
| 138 Returns: | |
| 139 A test case with the test case name or None if the test case | |
| 140 cannot be found. | |
| 141 """ | |
| 142 for testcase in self.testcases: | |
| 143 # Test case name is stored in the first column. | |
| 144 if testcase[0] == target: | |
| 145 return testcase | |
| 146 return None | |
| 147 | |
| 148 def get_all_test_case_names(self): | |
| 149 """Get all test case names. | |
| 150 | |
| 151 Returns: | |
| 152 A list of test case names. | |
| 153 """ | |
| 154 return [testcase[0] for testcase in self.testcases] | |
| 155 | |
| 156 def generate_link_for_bug(self, bug): | |
| 157 """Generate link for a bug. | |
| 158 | |
| 159 Parse the bug description accordingly. The bug description can be like | |
| 160 the following: BUGWK1234, BUGCR1234, BUGFOO. | |
| 161 | |
| 162 Args: | |
| 163 bug: A string bug description | |
| 164 | |
| 165 Returns: | |
| 166 A string that represents a bug link. Returns an empty | |
| 167 string if match is not found. | |
| 168 """ | |
| 169 pattern_for_webkit_bug = 'BUGWK(\d+)' | |
| 170 match = re.search(pattern_for_webkit_bug, bug) | |
| 171 if match is not None: | |
| 172 return self.WEBKIT_BUG_URL + match.group(1) | |
| 173 pattern_for_chrome_bug = 'BUGCR(\d+)' | |
| 174 match = re.search(pattern_for_chrome_bug, bug) | |
| 175 if match is not None: | |
| 176 return self.CHROME_BUG_URL + match.group(1) | |
| 177 pattern_for_other_bug = 'BUG(\S+)' | |
| 178 match = re.search(pattern_for_other_bug, bug) | |
| 179 if match is not None: | |
| 180 return 'mailto:%s@chromium.org' % match.group(1).lower() | |
| 181 return '' | |
| 182 | |
| 183 def generate_link_for_dashboard(self, testcase_name, webkit): | |
| 184 """Generate link for flakiness dashboard. | |
| 185 | |
| 186 Args: | |
| 187 testcase_name: a string test case name. | |
| 188 webkit: a boolean indicating whether to use the webkit dashboard. | |
| 189 | |
| 190 Returns: | |
| 191 A string link to the flakiness dashboard. | |
| 192 """ | |
| 193 url = self.FLAKINESS_DASHBOARD_LINK + urllib.quote(testcase_name) | |
| 194 if webkit: | |
| 195 url += '&master=webkit.org' | |
| 196 return url | |
| 197 | |
| 198 def parse_line(self, line, previous_comment_info_list, test_case_patterns, | |
| 199 media_test_cases_only, writer): | |
| 200 """Parse each line in test_expectations.txt. | |
| 201 | |
| 202 The format of each line is as follows: | |
| 203 BUG[0-9]+ [SKIP] [WONTFIX] [SLOW] [<platform>] [<config>] | |
| 204 : <url> = <EXPCTATION> | |
| 205 The example is: | |
| 206 BUG123 BUG345 MAC : media/hoge.html | |
| 207 WONTFIX SKIP BUG19635 : media/restore-from-page-cache.html | |
| 208 = TIMEOUT | |
| 209 | |
| 210 Args: | |
| 211 line: a text for each line in the test expectation file. | |
| 212 previous_comment_info_list: a list of comments in previous lines. | |
| 213 test_case_patterns: a string for test case pattern. | |
| 214 media_test_cases_only: a boolean for media test case only. | |
| 215 writer: writer for intermediate results. | |
| 216 """ | |
| 217 bugs = re.findall(r'BUG\w+', line) | |
| 218 testcases = re.search(r':\s+(\S+.[html|xml|svg|js])', line) | |
| 219 if testcases is not None: | |
| 220 testcase = testcases.group(1) | |
| 221 data = [] | |
| 222 data.append(testcase) | |
| 223 | |
| 224 if LayoutTestCaseManager.check_test_case_matches_pattern( | |
| 225 testcase, test_case_patterns): | |
| 226 data.append('Y') | |
| 227 media_test_case = True | |
| 228 else: | |
| 229 data.append('N') | |
| 230 media_test_case = False | |
| 231 | |
| 232 dlink = self.generate_link_for_dashboard(testcase, False) | |
| 233 # Generate dashboard link. | |
| 234 data.append( | |
| 235 CsvUtils.generate_hyperlink_in_csv(dlink, 'Y', '')) | |
| 236 dlink = self.generate_link_for_dashboard(testcase, True) | |
| 237 # Generate dashboard link. | |
| 238 data.append( | |
| 239 CsvUtils.generate_hyperlink_in_csv(dlink, 'Y', '')) | |
| 240 for bug in bugs: | |
| 241 blink = self.generate_link_for_bug(bug) | |
| 242 data.append( | |
| 243 CsvUtils.generate_hyperlink_in_csv(blink, bug, '')) | |
| 244 # Fill the gap for test case with less bug/test. | |
| 245 for i in range(2 - len(bugs)): | |
| 246 data.append('') | |
| 247 | |
| 248 # Fill all data with 'N' (default). | |
| 249 for field_name in self.get_all_column_names(False, False): | |
| 250 if field_name in line: | |
| 251 data.append('Y') | |
| 252 else: | |
| 253 data.append('N') | |
| 254 # Comments will be accumulated in previous comments. | |
| 255 # This is for multiple-line comment. | |
| 256 for previous_comment_info in previous_comment_info_list: | |
| 257 data.append(previous_comment_info) | |
| 258 # Write only media related test case if specified. | |
| 259 if (media_test_case and media_test_cases_only | |
| 260 or (not media_test_cases_only)): | |
| 261 writer.writerow(data) | |
| 262 self.testcases.append(data) | |
| 263 | |
| 264 def process_comments(self, comments): | |
| 265 """Process comments in the test expectations file. | |
| 266 | |
| 267 Comments may contain special keywords such as UNIMPLEMENTED, | |
| 268 KNOWNISSUE, TESTISSUE (in TestExpectationsManager.COMMENT_COLUMN_NAMES) | |
| 269 and may be multiple lines. | |
| 270 | |
| 271 Args: | |
| 272 comments: A raw comment from the test expectation file. | |
| 273 It is above test case name. | |
| 274 | |
| 275 Returns: | |
| 276 A list of 'Y' or 'N' whether the comments contain | |
| 277 each column name in TestExpectationsManager.COMMENT_COLUMN_NAMES | |
| 278 in the comments. | |
| 279 """ | |
| 280 return_list = [] | |
| 281 for ccn in TestExpectationsManager.COMMENT_COLUMN_NAMES: | |
| 282 if ccn in comments: | |
| 283 return_list.append('Y') | |
| 284 else: | |
| 285 return_list.append('N') | |
| 286 return_list.append(comments) | |
| 287 return return_list | |
| 288 | |
| 289 def get_and_save_content(self, location, output): | |
| 290 """Simply get test expectation from the specified location and save it. | |
| 291 | |
| 292 Args: | |
| 293 location: SVN location of the test expectation file. | |
| 294 output: an output file path including file name. | |
| 295 """ | |
| 296 client = pysvn.Client() | |
| 297 file_object = open(output, 'w') | |
| 298 file_object.write(client.cat(location)) | |
| 299 file_object.close() | |
| 300 | |
| 301 def get_and_save_content_media_only(self, location, output): | |
| 302 """Simply get test expectation from the specified location. | |
| 303 | |
| 304 It also saves it (media only). | |
| 305 | |
| 306 Args: | |
| 307 location: SVN location of the test expectation file. | |
| 308 output: an output file path including file name. | |
| 309 """ | |
| 310 file_object = file(location, 'r') | |
| 311 text_list = list(file_object) | |
| 312 output_text = '' | |
| 313 for txt in text_list: | |
| 314 if txt.startswith('//'): | |
| 315 output_text += txt | |
| 316 else: | |
| 317 for pattern in ( | |
| 318 self.TEST_CASE_PATTERNS.get_test_case_pattern( | |
| 319 'media')): | |
| 320 if re.search(pattern, txt) is not None: | |
| 321 output_text += txt | |
| 322 break | |
| 323 file_object.close() | |
| 324 # Save output. | |
| 325 file_object = open(output, 'w') | |
| 326 file_object.write(output_text) | |
| 327 file_object.close() | |
| 328 | |
| 329 def get_and_parse_content(self, location, output, media_test_case_only): | |
| 330 """Get test_expectations.txt and parse the content. | |
| 331 | |
| 332 The comments are parsed as well since it contains keywords. | |
| 333 | |
| 334 Args: | |
| 335 location: SVN location of the test expectation file. | |
| 336 output: an output file path including file name. | |
| 337 media_test_case_only: A boolean indicating whether media | |
| 338 test cases should only be processed. | |
| 339 """ | |
| 340 if location.startswith('http'): | |
| 341 # Get from SVN. | |
| 342 client = pysvn.Client() | |
| 343 # Check out the current version of the pysvn project. | |
| 344 txt = client.cat(location) | |
| 345 else: | |
| 346 if location.endswith('.csv'): | |
| 347 # No parsing in the case of CSV file. | |
| 348 # Direct reading. | |
| 349 file_object = file(location, 'r') | |
| 350 self.testcases = list(csv.reader(file_object)) | |
| 351 file_object.close() | |
| 352 return | |
| 353 file_object = open(location, 'r') | |
| 354 txt = file_object.read() | |
| 355 file_object.close() | |
| 356 | |
| 357 file_object = file(output, 'wb') | |
| 358 writer = csv.writer(file_object) | |
| 359 writer.writerow(self.get_all_column_names(True, True)) | |
| 360 lines = txt.split('\n') | |
| 361 process = False | |
| 362 previous_comment = '' | |
| 363 previous_comment_list = [] | |
| 364 for line in lines: | |
| 365 if line.isspace() or str is '': | |
| 366 continue | |
| 367 line.strip() | |
| 368 if line.startswith('//'): | |
| 369 # There are comments. | |
| 370 line = line.replace('//', '') | |
| 371 if process is True: | |
| 372 previous_comment = line | |
| 373 else: | |
| 374 previous_comment = previous_comment + '\n' + line | |
| 375 previous_comment_list = self.process_comments(previous_comment) | |
| 376 process = False | |
| 377 else: | |
| 378 self.parse_line( | |
| 379 line, previous_comment_list, | |
| 380 self.TEST_CASE_PATTERNS.get_test_case_pattern('media'), | |
| 381 media_test_case_only, writer) | |
| 382 process = True | |
| 383 file_object.close() | |
| 384 | |
| 385 def get_te_diff_between_times(self, te_location, start, end, patterns, | |
| 386 change, checkchange): | |
| 387 """Get test expectation diff output for a given time period. | |
| 388 | |
| 389 Args: | |
| 390 te_location: SVN location of the test expectation file. | |
| 391 start: a date object for the start of the time period. | |
| 392 end: a date object for the end of the time period. | |
| 393 patterns: test case name patterns of the test cases that | |
| 394 we are interested in. This could be test case name | |
| 395 as it is in the case of exact matching. | |
| 396 change: the number of change in occurrence of test case in | |
| 397 test expectation. | |
| 398 checkchange: a boolean to indicate if we want to check the | |
| 399 change in occurrence as specified in |change| argument. | |
| 400 | |
| 401 Returns: | |
| 402 a list of tuples (old_revision, new_revision, | |
| 403 diff_line, author, date, commit_message) that matches | |
| 404 the condition. | |
| 405 """ | |
| 406 client = pysvn.Client() | |
| 407 client.checkout(te_location, 'tmp', recurse=False) | |
| 408 logs = client.log('tmp/test_expectations.txt', | |
| 409 revision_start=pysvn.Revision( | |
| 410 pysvn.opt_revision_kind.date, start), | |
| 411 revision_end=pysvn.Revision( | |
| 412 pysvn.opt_revision_kind.date, end)) | |
| 413 result_list = [] | |
| 414 for i in xrange(len(logs) - 1): | |
| 415 # PySVN.log returns logs in reverse chronological order. | |
| 416 new_rev = logs[i].revision.number | |
| 417 old_rev = logs[i + 1].revision.number | |
| 418 # Getting information about new revision. | |
| 419 author = logs[i].author | |
| 420 date = logs[i].date | |
| 421 message = logs[i].message | |
| 422 text = client.diff('/tmp', 'tmp/test_expectations.txt', | |
| 423 revision1=pysvn.Revision( | |
| 424 pysvn.opt_revision_kind.number, old_rev), | |
| 425 revision2=pysvn.Revision( | |
| 426 pysvn.opt_revision_kind.number, new_rev)) | |
| 427 lines = text.split('\n') | |
| 428 for line in lines: | |
| 429 for pattern in patterns: | |
| 430 matches = re.findall(pattern, line) | |
| 431 if matches: | |
| 432 if checkchange: | |
| 433 if ((line[0] == '+' and change > 0) or | |
| 434 (line[0] == '-' and change < 0)): | |
| 435 result_list.append((old_rev, new_rev, line, | |
| 436 author, date, message)) | |
| 437 else: | |
| 438 if line[0] == '+' or line[0] == '-': | |
| 439 result_list.append((old_rev, new_rev, line, | |
| 440 author, date, message)) | |
| 441 return result_list | |
| OLD | NEW |