blob: 998656908dd67485312360ccc1c9572477b8e6fd [file] [log] [blame]
Steve Blocka7e24c12009-10-30 11:49:00 +00001#!/usr/bin/env python
2#
Ben Murdoch3ef787d2012-04-12 10:51:47 +01003# Copyright 2012 the V8 project authors. All rights reserved.
Steve Blocka7e24c12009-10-30 11:49:00 +00004# Redistribution and use in source and binary forms, with or without
5# modification, are permitted provided that the following conditions are
6# met:
7#
8# * Redistributions of source code must retain the above copyright
9# notice, this list of conditions and the following disclaimer.
10# * Redistributions in binary form must reproduce the above
11# copyright notice, this list of conditions and the following
12# disclaimer in the documentation and/or other materials provided
13# with the distribution.
14# * Neither the name of Google Inc. nor the names of its
15# contributors may be used to endorse or promote products derived
16# from this software without specific prior written permission.
17#
18# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
19# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
20# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
21# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
22# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
23# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
24# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
25# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
26# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
27# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
28# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
29
Ben Murdochbb769b22010-08-11 14:56:33 +010030try:
31 import hashlib
32 md5er = hashlib.md5
33except ImportError, e:
34 import md5
35 md5er = md5.new
Steve Blocka7e24c12009-10-30 11:49:00 +000036
Ben Murdochbb769b22010-08-11 14:56:33 +010037
Ben Murdoch4a90d5f2016-03-22 12:00:34 +000038import json
Steve Blocka7e24c12009-10-30 11:49:00 +000039import optparse
40import os
41from os.path import abspath, join, dirname, basename, exists
Steve Blockd0582a62009-12-15 09:54:21 +000042import pickle
Steve Blocka7e24c12009-10-30 11:49:00 +000043import re
44import sys
45import subprocess
Ben Murdoch3ef787d2012-04-12 10:51:47 +010046import multiprocessing
Ben Murdoch589d6972011-11-30 16:04:58 +000047from subprocess import PIPE
Steve Blocka7e24c12009-10-30 11:49:00 +000048
Ben Murdoch4a90d5f2016-03-22 12:00:34 +000049from testrunner.local import statusfile
50from testrunner.local import testsuite
51from testrunner.local import utils
52
53# Special LINT rules diverging from default and reason.
54# build/header_guard: Our guards have the form "V8_FOO_H_", not "SRC_FOO_H_".
Steve Blocka7e24c12009-10-30 11:49:00 +000055# build/include_what_you_use: Started giving false positives for variables
Ben Murdoch4a90d5f2016-03-22 12:00:34 +000056# named "string" and "map" assuming that you needed to include STL headers.
Ben Murdochb8a8cc12014-11-26 15:28:44 +000057# TODO(bmeurer): Fix and re-enable readability/check
Steve Blocka7e24c12009-10-30 11:49:00 +000058
Ben Murdoch4a90d5f2016-03-22 12:00:34 +000059LINT_RULES = """
60-build/header_guard
61+build/include_alpha
62-build/include_what_you_use
63-build/namespaces
64-readability/check
65+readability/streams
66-runtime/references
67""".split()
Ben Murdoch3ef787d2012-04-12 10:51:47 +010068
Ben Murdoch4a90d5f2016-03-22 12:00:34 +000069LINT_OUTPUT_PATTERN = re.compile(r'^.+[:(]\d+[:)]|^Done processing')
70FLAGS_LINE = re.compile("//\s*Flags:.*--([A-z0-9-])+_[A-z0-9].*\n")
Ben Murdoch3ef787d2012-04-12 10:51:47 +010071
72def CppLintWorker(command):
73 try:
74 process = subprocess.Popen(command, stderr=subprocess.PIPE)
75 process.wait()
76 out_lines = ""
77 error_count = -1
78 while True:
79 out_line = process.stderr.readline()
80 if out_line == '' and process.poll() != None:
Ben Murdochb8a8cc12014-11-26 15:28:44 +000081 if error_count == -1:
82 print "Failed to process %s" % command.pop()
83 return 1
Ben Murdoch3ef787d2012-04-12 10:51:47 +010084 break
85 m = LINT_OUTPUT_PATTERN.match(out_line)
86 if m:
87 out_lines += out_line
88 error_count += 1
Ben Murdochb8a8cc12014-11-26 15:28:44 +000089 sys.stdout.write(out_lines)
Ben Murdoch3ef787d2012-04-12 10:51:47 +010090 return error_count
91 except KeyboardInterrupt:
92 process.kill()
93 except:
94 print('Error running cpplint.py. Please make sure you have depot_tools' +
95 ' in your $PATH. Lint check skipped.')
96 process.kill()
97
98
Steve Blockd0582a62009-12-15 09:54:21 +000099class FileContentsCache(object):
100
101 def __init__(self, sums_file_name):
102 self.sums = {}
103 self.sums_file_name = sums_file_name
104
105 def Load(self):
106 try:
107 sums_file = None
108 try:
109 sums_file = open(self.sums_file_name, 'r')
110 self.sums = pickle.load(sums_file)
Ben Murdochb8a8cc12014-11-26 15:28:44 +0000111 except:
112 # Cannot parse pickle for any reason. Not much we can do about it.
Steve Blockd0582a62009-12-15 09:54:21 +0000113 pass
114 finally:
115 if sums_file:
116 sums_file.close()
117
118 def Save(self):
119 try:
120 sums_file = open(self.sums_file_name, 'w')
121 pickle.dump(self.sums, sums_file)
Ben Murdochb8a8cc12014-11-26 15:28:44 +0000122 except:
123 # Failed to write pickle. Try to clean-up behind us.
124 if sums_file:
125 sums_file.close()
126 try:
127 os.unlink(self.sums_file_name)
128 except:
129 pass
Steve Blockd0582a62009-12-15 09:54:21 +0000130 finally:
131 sums_file.close()
132
133 def FilterUnchangedFiles(self, files):
134 changed_or_new = []
135 for file in files:
136 try:
137 handle = open(file, "r")
Ben Murdochbb769b22010-08-11 14:56:33 +0100138 file_sum = md5er(handle.read()).digest()
Steve Blockd0582a62009-12-15 09:54:21 +0000139 if not file in self.sums or self.sums[file] != file_sum:
140 changed_or_new.append(file)
141 self.sums[file] = file_sum
142 finally:
143 handle.close()
144 return changed_or_new
145
146 def RemoveFile(self, file):
147 if file in self.sums:
148 self.sums.pop(file)
149
150
Steve Blocka7e24c12009-10-30 11:49:00 +0000151class SourceFileProcessor(object):
152 """
153 Utility class that can run through a directory structure, find all relevant
154 files and invoke a custom check on the files.
155 """
156
157 def Run(self, path):
158 all_files = []
159 for file in self.GetPathsToSearch():
160 all_files += self.FindFilesIn(join(path, file))
161 if not self.ProcessFiles(all_files, path):
162 return False
163 return True
164
165 def IgnoreDir(self, name):
Ben Murdochb8a8cc12014-11-26 15:28:44 +0000166 return (name.startswith('.') or
167 name in ('buildtools', 'data', 'gmock', 'gtest', 'kraken',
168 'octane', 'sunspider'))
Steve Blocka7e24c12009-10-30 11:49:00 +0000169
170 def IgnoreFile(self, name):
171 return name.startswith('.')
172
173 def FindFilesIn(self, path):
174 result = []
175 for (root, dirs, files) in os.walk(path):
176 for ignored in [x for x in dirs if self.IgnoreDir(x)]:
177 dirs.remove(ignored)
178 for file in files:
179 if not self.IgnoreFile(file) and self.IsRelevant(file):
180 result.append(join(root, file))
181 return result
182
183
184class CppLintProcessor(SourceFileProcessor):
185 """
186 Lint files to check that they follow the google code style.
187 """
188
189 def IsRelevant(self, name):
190 return name.endswith('.cc') or name.endswith('.h')
191
192 def IgnoreDir(self, name):
193 return (super(CppLintProcessor, self).IgnoreDir(name)
194 or (name == 'third_party'))
195
196 IGNORE_LINT = ['flag-definitions.h']
Steve Blockd0582a62009-12-15 09:54:21 +0000197
Steve Blocka7e24c12009-10-30 11:49:00 +0000198 def IgnoreFile(self, name):
199 return (super(CppLintProcessor, self).IgnoreFile(name)
200 or (name in CppLintProcessor.IGNORE_LINT))
201
202 def GetPathsToSearch(self):
Emily Bernierd0a1eb72015-03-24 16:35:39 -0400203 return ['src', 'include', 'samples', join('test', 'cctest'),
204 join('test', 'unittests')]
Ben Murdochb8a8cc12014-11-26 15:28:44 +0000205
206 def GetCpplintScript(self, prio_path):
207 for path in [prio_path] + os.environ["PATH"].split(os.pathsep):
208 path = path.strip('"')
209 cpplint = os.path.join(path, "cpplint.py")
210 if os.path.isfile(cpplint):
211 return cpplint
212
213 return None
Steve Blocka7e24c12009-10-30 11:49:00 +0000214
215 def ProcessFiles(self, files, path):
Steve Blockd0582a62009-12-15 09:54:21 +0000216 good_files_cache = FileContentsCache('.cpplint-cache')
217 good_files_cache.Load()
218 files = good_files_cache.FilterUnchangedFiles(files)
219 if len(files) == 0:
220 print 'No changes in files detected. Skipping cpplint check.'
221 return True
222
Ben Murdoch4a90d5f2016-03-22 12:00:34 +0000223 filters = ",".join([n for n in LINT_RULES])
224 command = [sys.executable, 'cpplint.py', '--filter', filters]
Ben Murdochb8a8cc12014-11-26 15:28:44 +0000225 cpplint = self.GetCpplintScript(join(path, "tools"))
226 if cpplint is None:
227 print('Could not find cpplint.py. Make sure '
228 'depot_tools is installed and in the path.')
229 sys.exit(1)
230
Ben Murdoch4a90d5f2016-03-22 12:00:34 +0000231 command = [sys.executable, cpplint, '--filter', filters]
Steve Blockd0582a62009-12-15 09:54:21 +0000232
Ben Murdoch3ef787d2012-04-12 10:51:47 +0100233 commands = join([command + [file] for file in files])
234 count = multiprocessing.cpu_count()
235 pool = multiprocessing.Pool(count)
236 try:
237 results = pool.map_async(CppLintWorker, commands).get(999999)
238 except KeyboardInterrupt:
239 print "\nCaught KeyboardInterrupt, terminating workers."
240 sys.exit(1)
Steve Blockd0582a62009-12-15 09:54:21 +0000241
Ben Murdoch3ef787d2012-04-12 10:51:47 +0100242 for i in range(len(files)):
243 if results[i] > 0:
244 good_files_cache.RemoveFile(files[i])
245
246 total_errors = sum(results)
247 print "Total errors found: %d" % total_errors
Steve Blockd0582a62009-12-15 09:54:21 +0000248 good_files_cache.Save()
Ben Murdoch3ef787d2012-04-12 10:51:47 +0100249 return total_errors == 0
Steve Blocka7e24c12009-10-30 11:49:00 +0000250
251
252COPYRIGHT_HEADER_PATTERN = re.compile(
Leon Clarkee46be812010-01-19 14:06:41 +0000253 r'Copyright [\d-]*20[0-1][0-9] the V8 project authors. All rights reserved.')
Steve Blocka7e24c12009-10-30 11:49:00 +0000254
255class SourceProcessor(SourceFileProcessor):
256 """
Ben Murdoch589d6972011-11-30 16:04:58 +0000257 Check that all files include a copyright notice and no trailing whitespaces.
Steve Blocka7e24c12009-10-30 11:49:00 +0000258 """
259
Ben Murdochb8a8cc12014-11-26 15:28:44 +0000260 RELEVANT_EXTENSIONS = ['.js', '.cc', '.h', '.py', '.c',
261 '.status', '.gyp', '.gypi']
Ben Murdoch589d6972011-11-30 16:04:58 +0000262
263 # Overwriting the one in the parent class.
264 def FindFilesIn(self, path):
265 if os.path.exists(path+'/.git'):
266 output = subprocess.Popen('git ls-files --full-name',
267 stdout=PIPE, cwd=path, shell=True)
268 result = []
269 for file in output.stdout.read().split():
Ben Murdochb8a8cc12014-11-26 15:28:44 +0000270 for dir_part in os.path.dirname(file).replace(os.sep, '/').split('/'):
Ben Murdoch589d6972011-11-30 16:04:58 +0000271 if self.IgnoreDir(dir_part):
272 break
273 else:
Ben Murdochb8a8cc12014-11-26 15:28:44 +0000274 if (self.IsRelevant(file) and os.path.exists(file)
275 and not self.IgnoreFile(file)):
Ben Murdoch589d6972011-11-30 16:04:58 +0000276 result.append(join(path, file))
277 if output.wait() == 0:
278 return result
279 return super(SourceProcessor, self).FindFilesIn(path)
280
Steve Blocka7e24c12009-10-30 11:49:00 +0000281 def IsRelevant(self, name):
282 for ext in SourceProcessor.RELEVANT_EXTENSIONS:
283 if name.endswith(ext):
284 return True
285 return False
286
287 def GetPathsToSearch(self):
288 return ['.']
289
290 def IgnoreDir(self, name):
Ben Murdochb8a8cc12014-11-26 15:28:44 +0000291 return (super(SourceProcessor, self).IgnoreDir(name) or
292 name in ('third_party', 'gyp', 'out', 'obj', 'DerivedSources'))
Steve Blocka7e24c12009-10-30 11:49:00 +0000293
Emily Bernierd0a1eb72015-03-24 16:35:39 -0400294 IGNORE_COPYRIGHTS = ['box2d.js',
295 'cpplint.py',
296 'copy.js',
297 'corrections.js',
298 'crypto.js',
Ben Murdochb8a8cc12014-11-26 15:28:44 +0000299 'daemon.py',
Ben Murdoch69a99ed2011-11-30 16:03:39 +0000300 'earley-boyer.js',
Emily Bernierd0a1eb72015-03-24 16:35:39 -0400301 'fannkuch.js',
302 'fasta.js',
303 'jsmin.py',
Ben Murdoch69a99ed2011-11-30 16:03:39 +0000304 'libraries.cc',
305 'libraries-empty.cc',
Emily Bernierd0a1eb72015-03-24 16:35:39 -0400306 'lua_binarytrees.js',
307 'memops.js',
Ben Murdoch4a90d5f2016-03-22 12:00:34 +0000308 'poppler.js',
Emily Bernierd0a1eb72015-03-24 16:35:39 -0400309 'primes.js',
310 'raytrace.js',
Ben Murdochb8a8cc12014-11-26 15:28:44 +0000311 'regexp-pcre.js',
Ben Murdoch4a90d5f2016-03-22 12:00:34 +0000312 'sqlite.js',
313 'sqlite-change-heap.js',
314 'sqlite-pointer-masking.js',
315 'sqlite-safe-heap.js',
Emily Bernierd0a1eb72015-03-24 16:35:39 -0400316 'gnuplot-4.6.3-emscripten.js',
317 'zlib.js']
Ben Murdoch69a99ed2011-11-30 16:03:39 +0000318 IGNORE_TABS = IGNORE_COPYRIGHTS + ['unicode-test.js', 'html-comments.js']
Steve Blocka7e24c12009-10-30 11:49:00 +0000319
Ben Murdochb8a8cc12014-11-26 15:28:44 +0000320 def EndOfDeclaration(self, line):
321 return line == "}" or line == "};"
322
323 def StartOfDeclaration(self, line):
324 return line.find("//") == 0 or \
325 line.find("/*") == 0 or \
326 line.find(") {") != -1
327
Steve Blocka7e24c12009-10-30 11:49:00 +0000328 def ProcessContents(self, name, contents):
329 result = True
330 base = basename(name)
331 if not base in SourceProcessor.IGNORE_TABS:
332 if '\t' in contents:
333 print "%s contains tabs" % name
334 result = False
335 if not base in SourceProcessor.IGNORE_COPYRIGHTS:
336 if not COPYRIGHT_HEADER_PATTERN.search(contents):
337 print "%s is missing a correct copyright header." % name
338 result = False
Ben Murdoch589d6972011-11-30 16:04:58 +0000339 if ' \n' in contents or contents.endswith(' '):
340 line = 0
341 lines = []
342 parts = contents.split(' \n')
343 if not contents.endswith(' '):
344 parts.pop()
345 for part in parts:
346 line += part.count('\n') + 1
347 lines.append(str(line))
348 linenumbers = ', '.join(lines)
349 if len(lines) > 1:
350 print "%s has trailing whitespaces in lines %s." % (name, linenumbers)
351 else:
352 print "%s has trailing whitespaces in line %s." % (name, linenumbers)
353 result = False
Ben Murdochb8a8cc12014-11-26 15:28:44 +0000354 if not contents.endswith('\n') or contents.endswith('\n\n'):
355 print "%s does not end with a single new line." % name
356 result = False
357 # Check two empty lines between declarations.
358 if name.endswith(".cc"):
359 line = 0
360 lines = []
361 parts = contents.split('\n')
362 while line < len(parts) - 2:
363 if self.EndOfDeclaration(parts[line]):
364 if self.StartOfDeclaration(parts[line + 1]):
365 lines.append(str(line + 1))
366 line += 1
367 elif parts[line + 1] == "" and \
368 self.StartOfDeclaration(parts[line + 2]):
369 lines.append(str(line + 1))
370 line += 2
371 line += 1
372 if len(lines) >= 1:
373 linenumbers = ', '.join(lines)
374 if len(lines) > 1:
375 print "%s does not have two empty lines between declarations " \
376 "in lines %s." % (name, linenumbers)
377 else:
378 print "%s does not have two empty lines between declarations " \
379 "in line %s." % (name, linenumbers)
380 result = False
Ben Murdoch4a90d5f2016-03-22 12:00:34 +0000381 # Sanitize flags for fuzzer.
382 if "mjsunit" in name:
383 match = FLAGS_LINE.search(contents)
384 if match:
385 print "%s Flags should use '-' (not '_')" % name
386 result = False
Steve Blocka7e24c12009-10-30 11:49:00 +0000387 return result
388
389 def ProcessFiles(self, files, path):
390 success = True
Ben Murdoch589d6972011-11-30 16:04:58 +0000391 violations = 0
Steve Blocka7e24c12009-10-30 11:49:00 +0000392 for file in files:
393 try:
394 handle = open(file)
395 contents = handle.read()
Ben Murdoch589d6972011-11-30 16:04:58 +0000396 if not self.ProcessContents(file, contents):
397 success = False
398 violations += 1
Steve Blocka7e24c12009-10-30 11:49:00 +0000399 finally:
400 handle.close()
Ben Murdoch589d6972011-11-30 16:04:58 +0000401 print "Total violating files: %s" % violations
Steve Blocka7e24c12009-10-30 11:49:00 +0000402 return success
403
404
Ben Murdochb8a8cc12014-11-26 15:28:44 +0000405def CheckExternalReferenceRegistration(workspace):
406 code = subprocess.call(
407 [sys.executable, join(workspace, "tools", "external-reference-check.py")])
408 return code == 0
409
410
Ben Murdoch4a90d5f2016-03-22 12:00:34 +0000411def _CheckStatusFileForDuplicateKeys(filepath):
412 comma_space_bracket = re.compile(", *]")
413 lines = []
414 with open(filepath) as f:
415 for line in f.readlines():
416 # Skip all-comment lines.
417 if line.lstrip().startswith("#"): continue
418 # Strip away comments at the end of the line.
419 comment_start = line.find("#")
420 if comment_start != -1:
421 line = line[:comment_start]
422 line = line.strip()
423 # Strip away trailing commas within the line.
424 line = comma_space_bracket.sub("]", line)
425 if len(line) > 0:
426 lines.append(line)
427
428 # Strip away trailing commas at line ends. Ugh.
429 for i in range(len(lines) - 1):
430 if (lines[i].endswith(",") and len(lines[i + 1]) > 0 and
431 lines[i + 1][0] in ("}", "]")):
432 lines[i] = lines[i][:-1]
433
434 contents = "\n".join(lines)
435 # JSON wants double-quotes.
436 contents = contents.replace("'", '"')
437 # Fill in keywords (like PASS, SKIP).
438 for key in statusfile.KEYWORDS:
439 contents = re.sub(r"\b%s\b" % key, "\"%s\"" % key, contents)
440
441 status = {"success": True}
442 def check_pairs(pairs):
443 keys = {}
444 for key, value in pairs:
445 if key in keys:
446 print("%s: Error: duplicate key %s" % (filepath, key))
447 status["success"] = False
448 keys[key] = True
449
450 json.loads(contents, object_pairs_hook=check_pairs)
451 return status["success"]
452
453def CheckStatusFiles(workspace):
454 success = True
455 suite_paths = utils.GetSuitePaths(join(workspace, "test"))
456 for root in suite_paths:
457 suite_path = join(workspace, "test", root)
458 status_file_path = join(suite_path, root + ".status")
459 suite = testsuite.TestSuite.LoadTestSuite(suite_path)
460 if suite and exists(status_file_path):
461 success &= statusfile.PresubmitCheck(status_file_path)
462 success &= _CheckStatusFileForDuplicateKeys(status_file_path)
463 return success
464
465def CheckAuthorizedAuthor(input_api, output_api):
466 """For non-googler/chromites committers, verify the author's email address is
467 in AUTHORS.
468 """
469 # TODO(maruel): Add it to input_api?
470 import fnmatch
471
472 author = input_api.change.author_email
473 if not author:
474 input_api.logging.info('No author, skipping AUTHOR check')
475 return []
476 authors_path = input_api.os_path.join(
477 input_api.PresubmitLocalPath(), 'AUTHORS')
478 valid_authors = (
479 input_api.re.match(r'[^#]+\s+\<(.+?)\>\s*$', line)
480 for line in open(authors_path))
481 valid_authors = [item.group(1).lower() for item in valid_authors if item]
482 if not any(fnmatch.fnmatch(author.lower(), valid) for valid in valid_authors):
483 input_api.logging.info('Valid authors are %s', ', '.join(valid_authors))
484 return [output_api.PresubmitPromptWarning(
485 ('%s is not in AUTHORS file. If you are a new contributor, please visit'
486 '\n'
487 'http://www.chromium.org/developers/contributing-code and read the '
488 '"Legal" section\n'
489 'If you are a chromite, verify the contributor signed the CLA.') %
490 author)]
491 return []
492
Steve Blocka7e24c12009-10-30 11:49:00 +0000493def GetOptions():
494 result = optparse.OptionParser()
495 result.add_option('--no-lint', help="Do not run cpplint", default=False,
496 action="store_true")
497 return result
498
499
500def Main():
501 workspace = abspath(join(dirname(sys.argv[0]), '..'))
502 parser = GetOptions()
503 (options, args) = parser.parse_args()
504 success = True
Ben Murdoch589d6972011-11-30 16:04:58 +0000505 print "Running C++ lint check..."
Steve Blocka7e24c12009-10-30 11:49:00 +0000506 if not options.no_lint:
Ben Murdoch4a90d5f2016-03-22 12:00:34 +0000507 success &= CppLintProcessor().Run(workspace)
Ben Murdochb8a8cc12014-11-26 15:28:44 +0000508 print "Running copyright header, trailing whitespaces and " \
509 "two empty lines between declarations check..."
Ben Murdoch4a90d5f2016-03-22 12:00:34 +0000510 success &= SourceProcessor().Run(workspace)
511 success &= CheckExternalReferenceRegistration(workspace)
512 success &= CheckStatusFiles(workspace)
Steve Blocka7e24c12009-10-30 11:49:00 +0000513 if success:
514 return 0
515 else:
516 return 1
517
518
519if __name__ == '__main__':
520 sys.exit(Main())