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""" |
dennis_jeffrey
2011/08/22 18:59:24
nit: add period at end of the sentence right after
imasaki1
2011/08/22 19:17:29
Done.
| |
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. In other words, this is the generated map after parsing both | |
53 lines: | |
54 {'media/video-zoom.html': [{'LINUX': True, 'DEBUG': True ....}, | |
55 {'MAC': True, 'GPU': True ....}] | |
56 which is produced from the lines: | |
57 BUGCR86714 MAC GPU : media/video-zoom.html = CRASH IMAGE | |
58 BUGCR86714 LINUX DEBUG : media/video-zoom.html = IMAGE | |
dennis_jeffrey
2011/08/22 18:59:24
This information is already given in lines 48-49 a
imasaki1
2011/08/22 19:17:29
Removed from here.
| |
59 """ | |
27 | 60 |
28 This class does the following: | 61 def __init__(self, url=DEFAULT_TEST_EXPECTATION_LOCATION): |
29 (1) get test expectation file from WebKit subversion source | 62 """Read the test expectation file from the specified URL and parse it. |
30 repository using pysvn. | 63 |
31 (2) parse the file and generate CSV entries. | 64 All parsed information is stored into instance variable |
32 (3) create hyperlinks where appropriate (e.g., test case name). | 65 |all_test_expectation_info|, which is a dictionary mapping a string test |
66 name to a list of dictionaries containing test expectation entry | |
67 information. An example of such dictionary: | |
68 {'media/video-zoom.html': [{'LINUX': True, 'DEBUG': True ....}, | |
69 {'MAC': True, 'GPU': True ....}] | |
70 which is produced from the lines: | |
71 BUGCR86714 MAC GPU : media/video-zoom.html = CRASH IMAGE | |
72 BUGCR86714 LINUX DEBUG : media/video-zoom.html = IMAGE | |
73 | |
74 Args: | |
75 url: A URL string for the test expectation file. | |
76 | |
77 Raises: | |
78 NameError when the test expectation file cannot be retrieved from |url|. | |
33 """ | 79 """ |
80 self.all_test_expectation_info = {} | |
81 resp = urllib2.urlopen(url) | |
82 if resp.code != 200: | |
83 raise NameError('Test expectation file does not exist in %s' % source) | |
84 # Start parsing each line. | |
85 comments = '' | |
86 for line in resp.read().split('\n'): | |
87 if line.startswith('//'): | |
88 # Comments can be multiple lines. | |
89 comments += line.replace('//', '') | |
90 elif not line: | |
91 comments = '' | |
92 else: | |
93 test_expectation_info = self.ParseLine(line, comments) | |
94 testname = TestExpectations.ExtractTestOrDirectoryName(line) | |
95 if not testname in self.all_test_expectation_info: | |
96 self.all_test_expectation_info[testname] = [] | |
97 # This is a list for multiple entries. | |
98 self.all_test_expectation_info[testname].append(test_expectation_info) | |
34 | 99 |
35 DEFAULT_TEST_EXPECTATION_DIR = ( | 100 @staticmethod |
36 'http://svn.webkit.org/repository/webkit/trunk/' | 101 def ExtractTestOrDirectoryName(line): |
37 'LayoutTests/platform/chromium/') | 102 """Extract either a test name or a directory name from each line. |
38 | 103 |
39 DEFAULT_TEST_EXPECTATION_LOCATION = ( | 104 Please note the name in the test expectation entry can be test name or |
40 'http://svn.webkit.org/repository/webkit/trunk/' | 105 dictionary: Such examples are: |
dennis_jeffrey
2011/08/22 18:59:24
'dictionary' --> 'directory'
| |
41 'LayoutTests/platform/chromium/test_expectations.txt') | 106 BUGWK43668 SKIP : media/track/ = TIMEOUT |
42 | 107 |
43 CHROME_BUG_URL = 'http://code.google.com/p/chromium/issues/detail?id=' | 108 Args: |
109 line: a line in the test expectation file. | |
44 | 110 |
45 WEBKIT_BUG_URL = 'https://bugs.webkit.org/show_bug.cgi?id=' | 111 Returns: |
112 a test name or directory name string. Returns '' if no match. | |
46 | 113 |
47 FLAKINESS_DASHBOARD_LINK = ( | 114 Raises: |
48 'http://test-results.appspot.com/dashboards/' | 115 ValueError when there is no test name match. |
49 'flakiness_dashboard.html#tests=') | 116 """ |
117 # First try to find test name ending with .html. | |
118 matches = re.search(r':\s+(\S+.html)', line) | |
119 # Next try to find directory name. | |
120 if matches: | |
121 return matches.group(1) | |
122 matches = re.search(r':\s+(\S+)', line) | |
123 if matches: | |
124 return matches.group(1) | |
125 else: | |
126 raise ValueError('test or dictionary name cannot be found in the line') | |
50 | 127 |
51 TEST_CASE_PATTERNS = TestCasePatterns() | 128 @staticmethod |
129 def ParseLine(line, comment_prefix): | |
130 """Parse each line in test expectation and update test expectation info. | |
52 | 131 |
53 OTHER_FIELD_NAMES = ['TestCase', 'Media', 'Flaky', 'WebKitD', 'Bug', | 132 This function checks for each entry from |ALL_TE_KEYWORDS| in the current |
54 'Test'] | 133 line and stores it in the test expectation info map if found. Comment |
134 and bug information is also stored in the map. | |
55 | 135 |
56 # The following is from test expectation syntax. | 136 Args: |
57 # BUG[0-9]+ [SKIP] [WONTFIX] [SLOW] | 137 line: a line in the test expectation file. For example, |
58 DECISION_COLUMN_NAMES = ['SKIP', 'SLOW'] | 138 "BUGCR86714 MAC GPU : media/video-zoom.html = CRASH IMAGE" |
59 # <platform> ::== [GPU] [CPU] [WIN] [LINUX] [MAC] | 139 comment_prefix: comments from the test expectation file occurring just |
60 PLATFORM_COLUMN_NAMES = ['GPU', 'CPU', 'WIN', 'LINUX', 'MAC'] | 140 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 | 141 |
69 # These are coming from metadata in comments in the test expectation file. | 142 Returns: |
70 COMMENT_COLUMN_NAMES = ['UNIMPLEMENTED', 'KNOWNISSUE', 'TESTISSUE', | 143 a dictionary containing test expectation info, including comment and bug |
71 'WONTFIX'] | 144 info. |
72 | 145 """ |
73 RAW_COMMENT_COLUMN_NAME = 'COMMENTS' | 146 test_expectation_info = {} |
74 | 147 # Store comments. |
75 def __init__(self): | 148 inline_comments = '' |
76 """Initialize the test cases.""" | 149 if '//' in line: |
77 self.testcases = [] | 150 inline_comments = line[line.rindex('//') + 2:] |
78 | 151 # Remove the inline comments to avoid the case where keywords are in |
79 def get_column_indexes(self, column_names): | 152 # inline comments. |
80 """Get column indexes for given column names. | 153 line = line[0:line.rindex('//')] |
81 | 154 for name in ALL_TE_KEYWORDS: |
82 Args: | 155 if name in line: |
83 column_names: a list of column names. | 156 test_expectation_info[name] = True |
84 | 157 test_expectation_info['Comments'] = comment_prefix + inline_comments |
85 Returns: | 158 # Store bug informations. |
86 a list of indexes for the given column names. | 159 bugs = re.findall(r'BUG\w+', line) |
87 """ | 160 if bugs: |
88 all_field_names = self.get_all_column_names(True, True) | 161 test_expectation_info['Bugs'] = bugs |
89 return [all_field_names.index( | 162 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 |