deps: update v8 to 4.3.61.21
[platform/upstream/nodejs.git] / deps / v8 / tools / run_perf.py
1 #!/usr/bin/env python
2 # Copyright 2014 the V8 project 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 """
7 Performance runner for d8.
8
9 Call e.g. with tools/run-perf.py --arch ia32 some_suite.json
10
11 The suite json format is expected to be:
12 {
13   "path": <relative path chunks to perf resources and main file>,
14   "name": <optional suite name, file name is default>,
15   "archs": [<architecture name for which this suite is run>, ...],
16   "binary": <name of binary to run, default "d8">,
17   "flags": [<flag to d8>, ...],
18   "test_flags": [<flag to the test file>, ...],
19   "run_count": <how often will this suite run (optional)>,
20   "run_count_XXX": <how often will this suite run for arch XXX (optional)>,
21   "resources": [<js file to be moved to android device>, ...]
22   "main": <main js perf runner file>,
23   "results_regexp": <optional regexp>,
24   "results_processor": <optional python results processor script>,
25   "units": <the unit specification for the performance dashboard>,
26   "tests": [
27     {
28       "name": <name of the trace>,
29       "results_regexp": <optional more specific regexp>,
30       "results_processor": <optional python results processor script>,
31       "units": <the unit specification for the performance dashboard>,
32     }, ...
33   ]
34 }
35
36 The tests field can also nest other suites in arbitrary depth. A suite
37 with a "main" file is a leaf suite that can contain one more level of
38 tests.
39
40 A suite's results_regexp is expected to have one string place holder
41 "%s" for the trace name. A trace's results_regexp overwrites suite
42 defaults.
43
44 A suite's results_processor may point to an optional python script. If
45 specified, it is called after running the tests like this (with a path
46 relatve to the suite level's path):
47 <results_processor file> <same flags as for d8> <suite level name> <output>
48
49 The <output> is a temporary file containing d8 output. The results_regexp will
50 be applied to the output of this script.
51
52 A suite without "tests" is considered a performance test itself.
53
54 Full example (suite with one runner):
55 {
56   "path": ["."],
57   "flags": ["--expose-gc"],
58   "test_flags": ["5"],
59   "archs": ["ia32", "x64"],
60   "run_count": 5,
61   "run_count_ia32": 3,
62   "main": "run.js",
63   "results_regexp": "^%s: (.+)$",
64   "units": "score",
65   "tests": [
66     {"name": "Richards"},
67     {"name": "DeltaBlue"},
68     {"name": "NavierStokes",
69      "results_regexp": "^NavierStokes: (.+)$"}
70   ]
71 }
72
73 Full example (suite with several runners):
74 {
75   "path": ["."],
76   "flags": ["--expose-gc"],
77   "archs": ["ia32", "x64"],
78   "run_count": 5,
79   "units": "score",
80   "tests": [
81     {"name": "Richards",
82      "path": ["richards"],
83      "main": "run.js",
84      "run_count": 3,
85      "results_regexp": "^Richards: (.+)$"},
86     {"name": "NavierStokes",
87      "path": ["navier_stokes"],
88      "main": "run.js",
89      "results_regexp": "^NavierStokes: (.+)$"}
90   ]
91 }
92
93 Path pieces are concatenated. D8 is always run with the suite's path as cwd.
94
95 The test flags are passed to the js test file after '--'.
96 """
97
98 from collections import OrderedDict
99 import json
100 import logging
101 import math
102 import optparse
103 import os
104 import re
105 import sys
106
107 from testrunner.local import commands
108 from testrunner.local import utils
109
110 ARCH_GUESS = utils.DefaultArch()
111 SUPPORTED_ARCHS = ["android_arm",
112                    "android_arm64",
113                    "android_ia32",
114                    "android_x64",
115                    "arm",
116                    "ia32",
117                    "mips",
118                    "mipsel",
119                    "nacl_ia32",
120                    "nacl_x64",
121                    "x64",
122                    "arm64"]
123
124 GENERIC_RESULTS_RE = re.compile(r"^RESULT ([^:]+): ([^=]+)= ([^ ]+) ([^ ]*)$")
125 RESULT_STDDEV_RE = re.compile(r"^\{([^\}]+)\}$")
126 RESULT_LIST_RE = re.compile(r"^\[([^\]]+)\]$")
127
128
129 def LoadAndroidBuildTools(path):  # pragma: no cover
130   assert os.path.exists(path)
131   sys.path.insert(0, path)
132
133   from pylib.device import device_utils  # pylint: disable=F0401
134   from pylib.device import device_errors  # pylint: disable=F0401
135   from pylib.perf import cache_control  # pylint: disable=F0401
136   from pylib.perf import perf_control  # pylint: disable=F0401
137   import pylib.android_commands  # pylint: disable=F0401
138   global cache_control
139   global device_errors
140   global device_utils
141   global perf_control
142   global pylib
143
144
145 def GeometricMean(values):
146   """Returns the geometric mean of a list of values.
147
148   The mean is calculated using log to avoid overflow.
149   """
150   values = map(float, values)
151   return str(math.exp(sum(map(math.log, values)) / len(values)))
152
153
154 class Results(object):
155   """Place holder for result traces."""
156   def __init__(self, traces=None, errors=None):
157     self.traces = traces or []
158     self.errors = errors or []
159
160   def ToDict(self):
161     return {"traces": self.traces, "errors": self.errors}
162
163   def WriteToFile(self, file_name):
164     with open(file_name, "w") as f:
165       f.write(json.dumps(self.ToDict()))
166
167   def __add__(self, other):
168     self.traces += other.traces
169     self.errors += other.errors
170     return self
171
172   def __str__(self):  # pragma: no cover
173     return str(self.ToDict())
174
175
176 class Node(object):
177   """Represents a node in the suite tree structure."""
178   def __init__(self, *args):
179     self._children = []
180
181   def AppendChild(self, child):
182     self._children.append(child)
183
184
185 class DefaultSentinel(Node):
186   """Fake parent node with all default values."""
187   def __init__(self):
188     super(DefaultSentinel, self).__init__()
189     self.binary = "d8"
190     self.run_count = 10
191     self.timeout = 60
192     self.path = []
193     self.graphs = []
194     self.flags = []
195     self.test_flags = []
196     self.resources = []
197     self.results_regexp = None
198     self.stddev_regexp = None
199     self.units = "score"
200     self.total = False
201
202
203 class Graph(Node):
204   """Represents a suite definition.
205
206   Can either be a leaf or an inner node that provides default values.
207   """
208   def __init__(self, suite, parent, arch):
209     super(Graph, self).__init__()
210     self._suite = suite
211
212     assert isinstance(suite.get("path", []), list)
213     assert isinstance(suite["name"], basestring)
214     assert isinstance(suite.get("flags", []), list)
215     assert isinstance(suite.get("test_flags", []), list)
216     assert isinstance(suite.get("resources", []), list)
217
218     # Accumulated values.
219     self.path = parent.path[:] + suite.get("path", [])
220     self.graphs = parent.graphs[:] + [suite["name"]]
221     self.flags = parent.flags[:] + suite.get("flags", [])
222     self.test_flags = parent.test_flags[:] + suite.get("test_flags", [])
223
224     # Values independent of parent node.
225     self.resources = suite.get("resources", [])
226
227     # Descrete values (with parent defaults).
228     self.binary = suite.get("binary", parent.binary)
229     self.run_count = suite.get("run_count", parent.run_count)
230     self.run_count = suite.get("run_count_%s" % arch, self.run_count)
231     self.timeout = suite.get("timeout", parent.timeout)
232     self.timeout = suite.get("timeout_%s" % arch, self.timeout)
233     self.units = suite.get("units", parent.units)
234     self.total = suite.get("total", parent.total)
235
236     # A regular expression for results. If the parent graph provides a
237     # regexp and the current suite has none, a string place holder for the
238     # suite name is expected.
239     # TODO(machenbach): Currently that makes only sense for the leaf level.
240     # Multiple place holders for multiple levels are not supported.
241     if parent.results_regexp:
242       regexp_default = parent.results_regexp % re.escape(suite["name"])
243     else:
244       regexp_default = None
245     self.results_regexp = suite.get("results_regexp", regexp_default)
246
247     # A similar regular expression for the standard deviation (optional).
248     if parent.stddev_regexp:
249       stddev_default = parent.stddev_regexp % re.escape(suite["name"])
250     else:
251       stddev_default = None
252     self.stddev_regexp = suite.get("stddev_regexp", stddev_default)
253
254
255 class Trace(Graph):
256   """Represents a leaf in the suite tree structure.
257
258   Handles collection of measurements.
259   """
260   def __init__(self, suite, parent, arch):
261     super(Trace, self).__init__(suite, parent, arch)
262     assert self.results_regexp
263     self.results = []
264     self.errors = []
265     self.stddev = ""
266
267   def ConsumeOutput(self, stdout):
268     try:
269       result = re.search(self.results_regexp, stdout, re.M).group(1)
270       self.results.append(str(float(result)))
271     except ValueError:
272       self.errors.append("Regexp \"%s\" returned a non-numeric for test %s."
273                          % (self.results_regexp, self.graphs[-1]))
274     except:
275       self.errors.append("Regexp \"%s\" didn't match for test %s."
276                          % (self.results_regexp, self.graphs[-1]))
277
278     try:
279       if self.stddev_regexp and self.stddev:
280         self.errors.append("Test %s should only run once since a stddev "
281                            "is provided by the test." % self.graphs[-1])
282       if self.stddev_regexp:
283         self.stddev = re.search(self.stddev_regexp, stdout, re.M).group(1)
284     except:
285       self.errors.append("Regexp \"%s\" didn't match for test %s."
286                          % (self.stddev_regexp, self.graphs[-1]))
287
288   def GetResults(self):
289     return Results([{
290       "graphs": self.graphs,
291       "units": self.units,
292       "results": self.results,
293       "stddev": self.stddev,
294     }], self.errors)
295
296
297 class Runnable(Graph):
298   """Represents a runnable suite definition (i.e. has a main file).
299   """
300   @property
301   def main(self):
302     return self._suite.get("main", "")
303
304   def ChangeCWD(self, suite_path):
305     """Changes the cwd to to path defined in the current graph.
306
307     The tests are supposed to be relative to the suite configuration.
308     """
309     suite_dir = os.path.abspath(os.path.dirname(suite_path))
310     bench_dir = os.path.normpath(os.path.join(*self.path))
311     os.chdir(os.path.join(suite_dir, bench_dir))
312
313   def GetCommandFlags(self):
314     suffix = ["--"] + self.test_flags if self.test_flags else []
315     return self.flags + [self.main] + suffix
316
317   def GetCommand(self, shell_dir):
318     # TODO(machenbach): This requires +.exe if run on windows.
319     return [os.path.join(shell_dir, self.binary)] + self.GetCommandFlags()
320
321   def Run(self, runner):
322     """Iterates over several runs and handles the output for all traces."""
323     for stdout in runner():
324       for trace in self._children:
325         trace.ConsumeOutput(stdout)
326     res = reduce(lambda r, t: r + t.GetResults(), self._children, Results())
327
328     if not res.traces or not self.total:
329       return res
330
331     # Assume all traces have the same structure.
332     if len(set(map(lambda t: len(t["results"]), res.traces))) != 1:
333       res.errors.append("Not all traces have the same number of results.")
334       return res
335
336     # Calculate the geometric means for all traces. Above we made sure that
337     # there is at least one trace and that the number of results is the same
338     # for each trace.
339     n_results = len(res.traces[0]["results"])
340     total_results = [GeometricMean(t["results"][i] for t in res.traces)
341                      for i in range(0, n_results)]
342     res.traces.append({
343       "graphs": self.graphs + ["Total"],
344       "units": res.traces[0]["units"],
345       "results": total_results,
346       "stddev": "",
347     })
348     return res
349
350 class RunnableTrace(Trace, Runnable):
351   """Represents a runnable suite definition that is a leaf."""
352   def __init__(self, suite, parent, arch):
353     super(RunnableTrace, self).__init__(suite, parent, arch)
354
355   def Run(self, runner):
356     """Iterates over several runs and handles the output."""
357     for stdout in runner():
358       self.ConsumeOutput(stdout)
359     return self.GetResults()
360
361
362 class RunnableGeneric(Runnable):
363   """Represents a runnable suite definition with generic traces."""
364   def __init__(self, suite, parent, arch):
365     super(RunnableGeneric, self).__init__(suite, parent, arch)
366
367   def Run(self, runner):
368     """Iterates over several runs and handles the output."""
369     traces = OrderedDict()
370     for stdout in runner():
371       for line in stdout.strip().splitlines():
372         match = GENERIC_RESULTS_RE.match(line)
373         if match:
374           stddev = ""
375           graph = match.group(1)
376           trace = match.group(2)
377           body = match.group(3)
378           units = match.group(4)
379           match_stddev = RESULT_STDDEV_RE.match(body)
380           match_list = RESULT_LIST_RE.match(body)
381           errors = []
382           if match_stddev:
383             result, stddev = map(str.strip, match_stddev.group(1).split(","))
384             results = [result]
385           elif match_list:
386             results = map(str.strip, match_list.group(1).split(","))
387           else:
388             results = [body.strip()]
389
390           try:
391             results = map(lambda r: str(float(r)), results)
392           except ValueError:
393             results = []
394             errors = ["Found non-numeric in %s" %
395                       "/".join(self.graphs + [graph, trace])]
396
397           trace_result = traces.setdefault(trace, Results([{
398             "graphs": self.graphs + [graph, trace],
399             "units": (units or self.units).strip(),
400             "results": [],
401             "stddev": "",
402           }], errors))
403           trace_result.traces[0]["results"].extend(results)
404           trace_result.traces[0]["stddev"] = stddev
405
406     return reduce(lambda r, t: r + t, traces.itervalues(), Results())
407
408
409 def MakeGraph(suite, arch, parent):
410   """Factory method for making graph objects."""
411   if isinstance(parent, Runnable):
412     # Below a runnable can only be traces.
413     return Trace(suite, parent, arch)
414   elif suite.get("main"):
415     # A main file makes this graph runnable.
416     if suite.get("tests"):
417       # This graph has subgraphs (traces).
418       return Runnable(suite, parent, arch)
419     else:
420       # This graph has no subgraphs, it's a leaf.
421       return RunnableTrace(suite, parent, arch)
422   elif suite.get("generic"):
423     # This is a generic suite definition. It is either a runnable executable
424     # or has a main js file.
425     return RunnableGeneric(suite, parent, arch)
426   elif suite.get("tests"):
427     # This is neither a leaf nor a runnable.
428     return Graph(suite, parent, arch)
429   else:  # pragma: no cover
430     raise Exception("Invalid suite configuration.")
431
432
433 def BuildGraphs(suite, arch, parent=None):
434   """Builds a tree structure of graph objects that corresponds to the suite
435   configuration.
436   """
437   parent = parent or DefaultSentinel()
438
439   # TODO(machenbach): Implement notion of cpu type?
440   if arch not in suite.get("archs", SUPPORTED_ARCHS):
441     return None
442
443   graph = MakeGraph(suite, arch, parent)
444   for subsuite in suite.get("tests", []):
445     BuildGraphs(subsuite, arch, graph)
446   parent.AppendChild(graph)
447   return graph
448
449
450 def FlattenRunnables(node, node_cb):
451   """Generator that traverses the tree structure and iterates over all
452   runnables.
453   """
454   node_cb(node)
455   if isinstance(node, Runnable):
456     yield node
457   elif isinstance(node, Node):
458     for child in node._children:
459       for result in FlattenRunnables(child, node_cb):
460         yield result
461   else:  # pragma: no cover
462     raise Exception("Invalid suite configuration.")
463
464
465 class Platform(object):
466   @staticmethod
467   def GetPlatform(options):
468     if options.arch.startswith("android"):
469       return AndroidPlatform(options)
470     else:
471       return DesktopPlatform(options)
472
473
474 class DesktopPlatform(Platform):
475   def __init__(self, options):
476     self.shell_dir = options.shell_dir
477
478   def PreExecution(self):
479     pass
480
481   def PostExecution(self):
482     pass
483
484   def PreTests(self, node, path):
485     if isinstance(node, Runnable):
486       node.ChangeCWD(path)
487
488   def Run(self, runnable, count):
489     output = commands.Execute(runnable.GetCommand(self.shell_dir),
490                               timeout=runnable.timeout)
491     print ">>> Stdout (#%d):" % (count + 1)
492     print output.stdout
493     if output.stderr:  # pragma: no cover
494       # Print stderr for debugging.
495       print ">>> Stderr (#%d):" % (count + 1)
496       print output.stderr
497     if output.timed_out:
498       print ">>> Test timed out after %ss." % runnable.timeout
499     return output.stdout
500
501
502 class AndroidPlatform(Platform):  # pragma: no cover
503   DEVICE_DIR = "/data/local/tmp/v8/"
504
505   def __init__(self, options):
506     self.shell_dir = options.shell_dir
507     LoadAndroidBuildTools(options.android_build_tools)
508
509     if not options.device:
510       # Detect attached device if not specified.
511       devices = pylib.android_commands.GetAttachedDevices(
512           hardware=True, emulator=False, offline=False)
513       assert devices and len(devices) == 1, (
514           "None or multiple devices detected. Please specify the device on "
515           "the command-line with --device")
516       options.device = devices[0]
517     adb_wrapper = pylib.android_commands.AndroidCommands(options.device)
518     self.device = device_utils.DeviceUtils(adb_wrapper)
519     self.adb = adb_wrapper.Adb()
520
521   def PreExecution(self):
522     perf = perf_control.PerfControl(self.device)
523     perf.SetHighPerfMode()
524
525     # Remember what we have already pushed to the device.
526     self.pushed = set()
527
528   def PostExecution(self):
529     perf = perf_control.PerfControl(self.device)
530     perf.SetDefaultPerfMode()
531     self.device.RunShellCommand(["rm", "-rf", AndroidPlatform.DEVICE_DIR])
532
533   def _SendCommand(self, cmd):
534     logging.info("adb -s %s %s" % (str(self.device), cmd))
535     return self.adb.SendCommand(cmd, timeout_time=60)
536
537   def _PushFile(self, host_dir, file_name, target_rel=".",
538                 skip_if_missing=False):
539     file_on_host = os.path.join(host_dir, file_name)
540     file_on_device_tmp = os.path.join(
541         AndroidPlatform.DEVICE_DIR, "_tmp_", file_name)
542     file_on_device = os.path.join(
543         AndroidPlatform.DEVICE_DIR, target_rel, file_name)
544     folder_on_device = os.path.dirname(file_on_device)
545
546     # Only attempt to push files that exist.
547     if not os.path.exists(file_on_host):
548       if not skip_if_missing:
549         logging.critical('Missing file on host: %s' % file_on_host)
550       return
551
552     # Only push files not yet pushed in one execution.
553     if file_on_host in self.pushed:
554       return
555     else:
556       self.pushed.add(file_on_host)
557
558     # Work-around for "text file busy" errors. Push the files to a temporary
559     # location and then copy them with a shell command.
560     output = self._SendCommand(
561         "push %s %s" % (file_on_host, file_on_device_tmp))
562     # Success looks like this: "3035 KB/s (12512056 bytes in 4.025s)".
563     # Errors look like this: "failed to copy  ... ".
564     if output and not re.search('^[0-9]', output.splitlines()[-1]):
565       logging.critical('PUSH FAILED: ' + output)
566     self._SendCommand("shell mkdir -p %s" % folder_on_device)
567     self._SendCommand("shell cp %s %s" % (file_on_device_tmp, file_on_device))
568
569   def PreTests(self, node, path):
570     suite_dir = os.path.abspath(os.path.dirname(path))
571     if node.path:
572       bench_rel = os.path.normpath(os.path.join(*node.path))
573       bench_abs = os.path.join(suite_dir, bench_rel)
574     else:
575       bench_rel = "."
576       bench_abs = suite_dir
577
578     self._PushFile(self.shell_dir, node.binary)
579
580     # Push external startup data. Backwards compatible for revisions where
581     # these files didn't exist.
582     self._PushFile(self.shell_dir, "natives_blob.bin", skip_if_missing=True)
583     self._PushFile(self.shell_dir, "snapshot_blob.bin", skip_if_missing=True)
584
585     if isinstance(node, Runnable):
586       self._PushFile(bench_abs, node.main, bench_rel)
587     for resource in node.resources:
588       self._PushFile(bench_abs, resource, bench_rel)
589
590   def Run(self, runnable, count):
591     cache = cache_control.CacheControl(self.device)
592     cache.DropRamCaches()
593     binary_on_device = AndroidPlatform.DEVICE_DIR + runnable.binary
594     cmd = [binary_on_device] + runnable.GetCommandFlags()
595
596     # Relative path to benchmark directory.
597     if runnable.path:
598       bench_rel = os.path.normpath(os.path.join(*runnable.path))
599     else:
600       bench_rel = "."
601
602     try:
603       output = self.device.RunShellCommand(
604           cmd,
605           cwd=os.path.join(AndroidPlatform.DEVICE_DIR, bench_rel),
606           timeout=runnable.timeout,
607           retries=0,
608       )
609       stdout = "\n".join(output)
610       print ">>> Stdout (#%d):" % (count + 1)
611       print stdout
612     except device_errors.CommandTimeoutError:
613       print ">>> Test timed out after %ss." % runnable.timeout
614       stdout = ""
615     return stdout
616
617
618 # TODO: Implement results_processor.
619 def Main(args):
620   logging.getLogger().setLevel(logging.INFO)
621   parser = optparse.OptionParser()
622   parser.add_option("--android-build-tools",
623                     help="Path to chromium's build/android.")
624   parser.add_option("--arch",
625                     help=("The architecture to run tests for, "
626                           "'auto' or 'native' for auto-detect"),
627                     default="x64")
628   parser.add_option("--buildbot",
629                     help="Adapt to path structure used on buildbots",
630                     default=False, action="store_true")
631   parser.add_option("--device",
632                     help="The device ID to run Android tests on. If not given "
633                          "it will be autodetected.")
634   parser.add_option("--json-test-results",
635                     help="Path to a file for storing json results.")
636   parser.add_option("--outdir", help="Base directory with compile output",
637                     default="out")
638   (options, args) = parser.parse_args(args)
639
640   if len(args) == 0:  # pragma: no cover
641     parser.print_help()
642     return 1
643
644   if options.arch in ["auto", "native"]:  # pragma: no cover
645     options.arch = ARCH_GUESS
646
647   if not options.arch in SUPPORTED_ARCHS:  # pragma: no cover
648     print "Unknown architecture %s" % options.arch
649     return 1
650
651   if (bool(options.arch.startswith("android")) !=
652       bool(options.android_build_tools)):  # pragma: no cover
653     print ("Android architectures imply setting --android-build-tools and the "
654            "other way around.")
655     return 1
656
657   if (options.device and not
658       options.arch.startswith("android")):  # pragma: no cover
659     print "Specifying a device requires an Android architecture to be used."
660     return 1
661
662   workspace = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
663
664   if options.buildbot:
665     options.shell_dir = os.path.join(workspace, options.outdir, "Release")
666   else:
667     options.shell_dir = os.path.join(workspace, options.outdir,
668                                      "%s.release" % options.arch)
669
670   platform = Platform.GetPlatform(options)
671
672   results = Results()
673   for path in args:
674     path = os.path.abspath(path)
675
676     if not os.path.exists(path):  # pragma: no cover
677       results.errors.append("Configuration file %s does not exist." % path)
678       continue
679
680     with open(path) as f:
681       suite = json.loads(f.read())
682
683     # If no name is given, default to the file name without .json.
684     suite.setdefault("name", os.path.splitext(os.path.basename(path))[0])
685
686     # Setup things common to one test suite.
687     platform.PreExecution()
688
689     # Build the graph/trace tree structure.
690     root = BuildGraphs(suite, options.arch)
691
692     # Callback to be called on each node on traversal.
693     def NodeCB(node):
694       platform.PreTests(node, path)
695
696     # Traverse graph/trace tree and interate over all runnables.
697     for runnable in FlattenRunnables(root, NodeCB):
698       print ">>> Running suite: %s" % "/".join(runnable.graphs)
699
700       def Runner():
701         """Output generator that reruns several times."""
702         for i in xrange(0, max(1, runnable.run_count)):
703           # TODO(machenbach): Allow timeout per arch like with run_count per
704           # arch.
705           yield platform.Run(runnable, i)
706
707       # Let runnable iterate over all runs and handle output.
708       results += runnable.Run(Runner)
709
710     platform.PostExecution()
711
712   if options.json_test_results:
713     results.WriteToFile(options.json_test_results)
714   else:  # pragma: no cover
715     print results
716
717   return min(1, len(results.errors))
718
719 if __name__ == "__main__":  # pragma: no cover
720   sys.exit(Main(sys.argv[1:]))