OLD | NEW |
---|---|
1 #!/usr/bin/env python | 1 #!/usr/bin/env python |
2 # Copyright 2014 the V8 project authors. All rights reserved. | 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 | 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 import itertools | |
7 import multiprocessing | |
8 import optparse | |
6 import os | 9 import os |
10 import random | |
7 import re | 11 import re |
8 import shutil | 12 import shutil |
13 import signal | |
14 import string | |
15 import subprocess | |
9 import sys | 16 import sys |
17 import time | |
10 | 18 |
11 # TODO(jkummerow): Support DATA_VIEW_{G,S}ETTER in runtime.cc | 19 # TODO(jkummerow): Support DATA_VIEW_{G,S}ETTER in runtime.cc |
12 | 20 |
13 FILENAME = "src/runtime.cc" | 21 FILENAME = "src/runtime.cc" |
14 HEADERFILENAME = "src/runtime.h" | 22 HEADERFILENAME = "src/runtime.h" |
15 FUNCTION = re.compile("^RUNTIME_FUNCTION\(Runtime_(\w+)") | 23 FUNCTION = re.compile("^RUNTIME_FUNCTION\(Runtime_(\w+)") |
16 ARGSLENGTH = re.compile(".*ASSERT\(.*args\.length\(\) == (\d+)\);") | 24 ARGSLENGTH = re.compile(".*ASSERT\(.*args\.length\(\) == (\d+)\);") |
17 FUNCTIONEND = "}\n" | 25 FUNCTIONEND = "}\n" |
18 | 26 |
19 WORKSPACE = os.path.abspath(os.path.join(os.path.dirname(sys.argv[0]), "..")) | 27 WORKSPACE = os.path.abspath(os.path.join(os.path.dirname(sys.argv[0]), "..")) |
(...skipping 129 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
149 } | 157 } |
150 | 158 |
151 | 159 |
152 # Types of arguments that cannot be generated in a JavaScript testcase. | 160 # Types of arguments that cannot be generated in a JavaScript testcase. |
153 NON_JS_TYPES = [ | 161 NON_JS_TYPES = [ |
154 "Code", "Context", "FixedArray", "FunctionTemplateInfo", | 162 "Code", "Context", "FixedArray", "FunctionTemplateInfo", |
155 "JSFunctionResultCache", "JSMessageObject", "Map", "ScopeInfo", | 163 "JSFunctionResultCache", "JSMessageObject", "Map", "ScopeInfo", |
156 "SharedFunctionInfo"] | 164 "SharedFunctionInfo"] |
157 | 165 |
158 | 166 |
159 # Maps argument types to concrete example inputs of that type. | 167 class Generator(object): |
160 JS_TYPE_GENERATORS = { | 168 |
161 "Boolean": "true", | 169 def RandomVariable(self, varname, vartype, simple): |
162 "HeapObject": "new Object()", | 170 if simple: |
163 "Int32": "32", | 171 return self._Variable(varname, self.GENERATORS[vartype][0]) |
164 "JSArray": "new Array()", | 172 return self.GENERATORS[vartype][1](self, varname, |
165 "JSArrayBuffer": "new ArrayBuffer(8)", | 173 self.DEFAULT_RECURSION_BUDGET) |
166 "JSDataView": "new DataView(new ArrayBuffer(8))", | 174 |
167 "JSDate": "new Date()", | 175 @staticmethod |
168 "JSFunction": "function() {}", | 176 def IsTypeSupported(typename): |
169 "JSFunctionProxy": "Proxy.createFunction({}, function() {})", | 177 return typename in Generator.GENERATORS |
170 "JSGeneratorObject": "(function*(){ yield 1; })()", | 178 |
171 "JSMap": "new Map()", | 179 USUAL_SUSPECT_PROPERTIES = ["size", "length", "byteLength", "__proto__", |
172 "JSMapIterator": "%MapCreateIterator(new Map(), 3)", | 180 "prototype"] |
173 "JSObject": "new Object()", | 181 DEFAULT_RECURSION_BUDGET = 2 |
174 "JSProxy": "Proxy.create({})", | 182 PROXY_TRAPS = """{ |
175 "JSReceiver": "new Object()", | 183 getOwnPropertyDescriptor: function(name) { |
176 "JSRegExp": "/ab/g", | 184 return {value: function() {}, configurable: true, writable: true, |
177 "JSSet": "new Set()", | 185 enumerable: true}; |
178 "JSSetIterator": "%SetCreateIterator(new Set(), 2)", | 186 }, |
179 "JSTypedArray": "new Int32Array(2)", | 187 getPropertyDescriptor: function(name) { |
180 "JSValue": "new String('foo')", | 188 return {value: function() {}, configurable: true, writable: true, |
181 "JSWeakCollection": "new WeakMap()", | 189 enumerable: true}; |
182 "Name": "\"name\"", | 190 }, |
183 "Number": "1.5", | 191 getOwnPropertyNames: function() { return []; }, |
184 "Object": "new Object()", | 192 getPropertyNames: function() { return []; }, |
185 "PropertyDetails": "513", | 193 defineProperty: function(name, descriptor) {}, |
186 "SeqString": "\"seqstring\"", | 194 delete: function(name) { return true; }, |
187 "Smi": 1, | 195 fix: function() {} |
188 "StrictMode": "1", | 196 }""" |
189 "String": "\"foo\"", | 197 |
190 "Symbol": "Symbol(\"symbol\")", | 198 def _Variable(self, name, value, fallback=None): |
191 "Uint32": "32", | 199 args = { "name": name, "value": value, "fallback": fallback } |
192 } | 200 if fallback: |
201 wrapper = "try { %%s } catch(e) { var %(name)s = %(fallback)s; }" % args | |
202 else: | |
203 wrapper = "%s" | |
204 return [wrapper % ("var %(name)s = %(value)s;" % args)] | |
205 | |
206 def _Boolean(self, name, recursion_budget): | |
207 return self._Variable(name, random.choice(["true", "false"])) | |
208 | |
209 def _Oddball(self, name, recursion_budget): | |
210 return self._Variable(name, | |
211 random.choice(["true", "false", "undefined", "null"])) | |
212 | |
213 def _StrictMode(self, name, recursion_budget): | |
214 return self._Variable(name, random.choice([0, 1])) | |
215 | |
216 def _Int32(self, name, recursion_budget=0): | |
217 die = random.random() | |
218 if die < 0.5: | |
219 value = random.choice([-3, -1, 0, 1, 2, 10, 515, 0x3fffffff, 0x7fffffff, | |
220 0x40000000, -0x40000000, -0x80000000]) | |
221 elif die < 0.75: | |
222 value = random.randint(-1000, 1000) | |
223 else: | |
224 value = random.randint(-0x80000000, 0x7fffffff) | |
225 return self._Variable(name, value) | |
226 | |
227 def _Uint32(self, name, recursion_budget=0): | |
228 die = random.random() | |
229 if die < 0.5: | |
230 value = random.choice([0, 1, 2, 3, 4, 8, 0x3fffffff, 0x40000000, | |
231 0x7fffffff, 0xffffffff]) | |
232 elif die < 0.75: | |
233 value = random.randint(0, 1000) | |
234 else: | |
235 value = random.randint(0, 0xffffffff) | |
236 return self._Variable(name, value) | |
237 | |
238 def _Smi(self, name, recursion_budget): | |
239 die = random.random() | |
240 if die < 0.5: | |
241 value = random.choice([-5, -1, 0, 1, 2, 3, 0x3fffffff, -0x40000000]) | |
242 elif die < 0.75: | |
243 value = random.randint(-1000, 1000) | |
244 else: | |
245 value = random.randint(-0x40000000, 0x3fffffff) | |
246 return self._Variable(name, value) | |
247 | |
248 def _Number(self, name, recursion_budget): | |
249 die = random.random() | |
250 if die < 0.5: | |
251 return self._Smi(name, recursion_budget) | |
252 elif die < 0.6: | |
253 value = random.choice(["Infinity", "-Infinity", "NaN", "-0"]) | |
254 else: | |
255 value = random.lognormvariate(0, 15) | |
Jarin
2014/05/14 11:55:08
We might also want to add smallest/largest positiv
Jakob Kummerow
2014/05/14 12:52:04
Done.
| |
256 return self._Variable(name, value) | |
257 | |
258 def _RawRandomString(self, minlength=0, maxlength=100, | |
259 alphabet=string.ascii_letters): | |
260 length = random.randint(minlength, maxlength) | |
261 result = "" | |
262 for i in xrange(length): | |
263 result += random.choice(alphabet) | |
264 return result | |
265 | |
266 def _SeqString(self, name, recursion_budget): | |
267 s1 = self._RawRandomString(1, 5) | |
268 s2 = self._RawRandomString(1, 5) | |
269 # 'foo' + 'bar' | |
270 return self._Variable(name, "\"%s\" + \"%s\"" % (s1, s2)) | |
271 | |
272 def _SlicedString(self, name): | |
273 s = self._RawRandomString(20, 30) | |
274 # 'ffoo12345678901234567890'.substr(1) | |
275 return self._Variable(name, "\"%s\".substr(1)" % s) | |
276 | |
277 def _ConsString(self, name): | |
278 s1 = self._RawRandomString(8, 15) | |
279 s2 = self._RawRandomString(8, 15) | |
280 # 'foo12345' + (function() { return 'bar12345';})() | |
281 return self._Variable(name, | |
282 "\"%s\" + (function() { return \"%s\";})()" % (s1, s2)) | |
283 | |
284 def _ExternalString(self, name): | |
285 # Needs --expose-externalize-string. | |
286 return None | |
287 | |
288 def _InternalizedString(self, name): | |
289 return self._Variable(name, "\"%s\"" % self._RawRandomString(0, 20)) | |
290 | |
291 def _String(self, name, recursion_budget): | |
Jarin
2014/05/14 11:55:08
Would it make sense to generate number strings? (P
Jakob Kummerow
2014/05/14 12:52:04
Done.
| |
292 die = random.random() | |
293 if die < 0.4: | |
294 string = random.choice(self.USUAL_SUSPECT_PROPERTIES) | |
295 return self._Variable(name, "\"%s\"" % string) | |
296 elif die < 0.55: | |
297 return self._SeqString(name, recursion_budget) | |
298 elif die < 0.7: | |
299 return self._ConsString(name) | |
300 elif die < 0.85: | |
301 return self._InternalizedString(name) | |
302 else: | |
303 return self._SlicedString(name) | |
304 | |
305 def _Symbol(self, name, recursion_budget): | |
306 raw_string_name = name + "_1" | |
307 result = self._String(raw_string_name, recursion_budget) | |
308 return result + self._Variable(name, "Symbol(%s)" % raw_string_name) | |
309 | |
310 def _Name(self, name, recursion_budget): | |
311 if random.random() < 0.2: | |
312 return self._Symbol(name, recursion_budget) | |
313 return self._String(name, recursion_budget) | |
314 | |
315 def _JSValue(self, name, recursion_budget): | |
316 die = random.random() | |
317 raw_name = name + "_1" | |
318 if die < 0.33: | |
319 result = self._String(raw_name, recursion_budget) | |
320 return result + self._Variable(name, "new String(%s)" % raw_name) | |
321 elif die < 0.66: | |
322 result = self._Boolean(raw_name, recursion_budget) | |
323 return result + self._Variable(name, "new Boolean(%s)" % raw_name) | |
324 else: | |
325 result = self._Number(raw_name, recursion_budget) | |
326 return result + self._Variable(name, "new Number(%s)" % raw_name) | |
327 | |
328 def _RawRandomPropertyName(self): | |
329 if random.random() < 0.5: | |
330 return random.choice(self.USUAL_SUSPECT_PROPERTIES) | |
331 return self._RawRandomString(0, 10) | |
332 | |
333 def _AddProperties(self, name, result, recursion_budget): | |
334 propcount = random.randint(0, 3) | |
335 propname = None | |
336 for i in range(propcount): | |
337 die = random.random() | |
338 if die < 0.5: | |
339 propname = "%s_prop%d" % (name, i) | |
340 result += self._Name(propname, recursion_budget - 1) | |
341 else: | |
342 propname = "\"%s\"" % self._RawRandomPropertyName() | |
343 propvalue_name = "%s_val%d" % (name, i) | |
344 result += self._Object(propvalue_name, recursion_budget - 1) | |
345 result.append("try { %s[%s] = %s; } catch (e) {}" % | |
346 (name, propname, propvalue_name)) | |
347 if random.random() < 0.2 and propname: | |
348 # Force the object to slow mode. | |
349 result.append("delete %s[%s];" % (name, propname)) | |
350 | |
351 def _RandomElementIndex(self, element_name, result): | |
352 if random.random() < 0.5: | |
353 return random.randint(-1000, 1000) | |
354 result += self._Smi(element_name, 0) | |
355 return element_name | |
356 | |
357 def _AddElements(self, name, result, recursion_budget): | |
358 elementcount = random.randint(0, 3) | |
359 for i in range(elementcount): | |
360 element_name = "%s_idx%d" % (name, i) | |
361 index = self._RandomElementIndex(element_name, result) | |
362 value_name = "%s_elt%d" % (name, i) | |
363 result += self._Object(value_name, recursion_budget - 1) | |
364 result.append("try { %s[%s] = %s; } catch(e) {}" % | |
365 (name, index, value_name)) | |
366 | |
367 def _AddAccessors(self, name, result, recursion_budget): | |
368 accessorcount = random.randint(0, 3) | |
369 for i in range(accessorcount): | |
370 propname = self._RawRandomPropertyName() | |
371 what = random.choice(["get", "set"]) | |
372 function_name = "%s_access%d" % (name, i) | |
373 result += self._PlainFunction(function_name, recursion_budget - 1) | |
374 result.append("try { Object.defineProperty(%s, \"%s\", {%s: %s}); } " | |
375 "catch (e) {}" % (name, propname, what, function_name)) | |
376 | |
377 def _PlainArray(self, name, recursion_budget): | |
378 die = random.random() | |
379 if die < 0.5: | |
380 literal = random.choice(["[]", "[1, 2]", "[1.5, 2.5]", | |
381 "['a', 'b', 1, true]"]) | |
382 return self._Variable(name, literal) | |
383 else: | |
384 new = random.choice(["", "new "]) | |
385 length = random.randint(0, 101000) | |
386 return self._Variable(name, "%sArray(%d)" % (new, length)) | |
387 | |
388 def _PlainObject(self, name, recursion_budget): | |
389 die = random.random() | |
390 if die < 0.67: | |
391 literal_propcount = random.randint(0, 3) | |
392 properties = [] | |
393 result = [] | |
394 for i in range(literal_propcount): | |
395 propname = self._RawRandomPropertyName() | |
396 propvalue_name = "%s_lit%d" % (name, i) | |
397 result += self._Object(propvalue_name, recursion_budget - 1) | |
398 properties.append("\"%s\": %s" % (propname, propvalue_name)) | |
399 return result + self._Variable(name, "{%s}" % ", ".join(properties)) | |
400 else: | |
401 return self._Variable(name, "new Object()") | |
402 | |
403 def _JSArray(self, name, recursion_budget): | |
404 result = self._PlainArray(name, recursion_budget) | |
405 self._AddAccessors(name, result, recursion_budget) | |
406 self._AddProperties(name, result, recursion_budget) | |
407 self._AddElements(name, result, recursion_budget) | |
408 return result | |
409 | |
410 def _RawRandomBufferLength(self): | |
411 if random.random() < 0.2: | |
412 return random.choice([0, 1, 8, 0x40000000, 0x80000000]) | |
413 return random.randint(0, 1000) | |
414 | |
415 def _JSArrayBuffer(self, name, recursion_budget): | |
416 length = self._RawRandomBufferLength() | |
417 return self._Variable(name, "new ArrayBuffer(%d)" % length) | |
418 | |
419 def _JSDataView(self, name, recursion_budget): | |
420 buffer_name = name + "_buffer" | |
421 result = self._JSArrayBuffer(buffer_name, recursion_budget) | |
422 args = [buffer_name] | |
423 die = random.random() | |
424 if die < 0.67: | |
425 offset = self._RawRandomBufferLength() | |
426 args.append("%d" % offset) | |
427 if die < 0.33: | |
428 length = self._RawRandomBufferLength() | |
429 args.append("%d" % length) | |
430 result += self._Variable(name, "new DataView(%s)" % ", ".join(args), | |
431 fallback="new DataView(new ArrayBuffer(8))") | |
432 return result | |
433 | |
434 def _JSDate(self, name, recursion_budget): | |
435 die = random.random() | |
436 if die < 0.25: | |
437 return self._Variable(name, "new Date()") | |
438 elif die < 0.5: | |
439 ms_name = name + "_ms" | |
440 result = self._Number(ms_name, recursion_budget) | |
441 return result + self._Variable(name, "new Date(%s)" % ms_name) | |
442 elif die < 0.75: | |
443 str_name = name + "_str" | |
444 month = random.choice(["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", | |
445 "Aug", "Sep", "Oct", "Nov", "Dec"]) | |
446 day = random.randint(1, 28) | |
447 year = random.randint(1900, 2100) | |
448 hour = random.randint(0, 23) | |
449 minute = random.randint(0, 59) | |
450 second = random.randint(0, 59) | |
451 str_value = ("\"%s %s, %s %s:%s:%s\"" % | |
452 (month, day, year, hour, minute, second)) | |
453 result = self._Variable(str_name, str_value) | |
454 return result + self._Variable(name, "new Date(%s)" % str_name) | |
455 else: | |
456 components = tuple(map(lambda x: "%s_%s" % (name, x), | |
457 ["y", "m", "d", "h", "min", "s", "ms"])) | |
458 return ([j for i in map(self._Int32, components) for j in i] + | |
459 self._Variable(name, "new Date(%s)" % ", ".join(components))) | |
460 | |
461 def _PlainFunction(self, name, recursion_budget): | |
462 result_name = "result" | |
463 body = ["function() {"] | |
464 body += self._Object(result_name, recursion_budget - 1) | |
465 body.append("return result;\n}") | |
466 return self._Variable(name, "%s" % "\n".join(body)) | |
467 | |
468 def _JSFunction(self, name, recursion_budget): | |
469 result = self._PlainFunction(name, recursion_budget) | |
470 self._AddAccessors(name, result, recursion_budget) | |
471 self._AddProperties(name, result, recursion_budget) | |
472 self._AddElements(name, result, recursion_budget) | |
473 return result | |
474 | |
475 def _JSFunctionProxy(self, name, recursion_budget): | |
476 # TODO(jkummerow): Revisit this as the Proxy implementation evolves. | |
477 return self._Variable(name, "Proxy.createFunction(%s, function() {})" % | |
478 self.PROXY_TRAPS) | |
479 | |
480 def _JSGeneratorObject(self, name, recursion_budget): | |
481 # TODO(jkummerow): Be more creative here? | |
482 return self._Variable(name, "(function*() { yield 1; })()") | |
483 | |
484 def _JSMap(self, name, recursion_budget, weak=""): | |
485 result = self._Variable(name, "new %sMap()" % weak) | |
486 num_entries = random.randint(0, 3) | |
487 for i in range(num_entries): | |
488 key_name = "%s_k%d" % (name, i) | |
489 value_name = "%s_v%d" % (name, i) | |
490 if weak: | |
491 result += self._JSObject(key_name, recursion_budget - 1) | |
492 else: | |
493 result += self._Object(key_name, recursion_budget - 1) | |
494 result += self._Object(value_name, recursion_budget - 1) | |
495 result.append("%s.set(%s, %s)" % (name, key_name, value_name)) | |
496 return result | |
497 | |
498 def _JSMapIterator(self, name, recursion_budget): | |
499 map_name = name + "_map" | |
500 result = self._JSMap(map_name, recursion_budget) | |
501 iterator_type = random.randint(1, 3) | |
502 return (result + self._Variable(name, "%%MapCreateIterator(%s, %d)" % | |
503 (map_name, iterator_type))) | |
504 | |
505 def _JSProxy(self, name, recursion_budget): | |
506 # TODO(jkummerow): Revisit this as the Proxy implementation evolves. | |
507 return self._Variable(name, "Proxy.create(%s)" % self.PROXY_TRAPS) | |
508 | |
509 def _JSRegExp(self, name, recursion_budget): | |
510 flags = random.choice(["", "g", "i", "m", "gi"]) | |
511 string = "a(b|c)*a" # TODO(jkummerow): Be more creative here? | |
512 ctor = random.choice(["/%s/%s", "new RegExp(\"%s\", \"%s\")"]) | |
513 return self._Variable(name, ctor % (string, flags)) | |
514 | |
515 def _JSSet(self, name, recursion_budget, weak=""): | |
516 result = self._Variable(name, "new %sSet()" % weak) | |
517 num_entries = random.randint(0, 3) | |
518 for i in range(num_entries): | |
519 element_name = "%s_e%d" % (name, i) | |
520 if weak: | |
521 result += self._JSObject(element_name, recursion_budget - 1) | |
522 else: | |
523 result += self._Object(element_name, recursion_budget - 1) | |
524 result.append("%s.add(%s)" % (name, element_name)) | |
525 return result | |
526 | |
527 def _JSSetIterator(self, name, recursion_budget): | |
528 set_name = name + "_set" | |
529 result = self._JSSet(set_name, recursion_budget) | |
530 iterator_type = random.randint(2, 3) | |
531 return (result + self._Variable(name, "%%SetCreateIterator(%s, %d)" % | |
532 (set_name, iterator_type))) | |
533 | |
534 def _JSTypedArray(self, name, recursion_budget): | |
535 arraytype = random.choice(["Int8", "Int16", "Int32", "Uint8", "Uint16", | |
536 "Uint32", "Float32", "Float64", "Uint8Clamped"]) | |
537 ctor_type = random.randint(0, 3) | |
538 if ctor_type == 0: | |
539 length = random.randint(0, 1000) | |
540 return self._Variable(name, "new %sArray(%d)" % (arraytype, length), | |
541 fallback="new %sArray(8)" % arraytype) | |
542 elif ctor_type == 1: | |
543 input_name = name + "_typedarray" | |
544 result = self._JSTypedArray(input_name, recursion_budget - 1) | |
545 return (result + | |
546 self._Variable(name, "new %sArray(%s)" % (arraytype, input_name), | |
547 fallback="new %sArray(8)" % arraytype)) | |
548 elif ctor_type == 2: | |
549 arraylike_name = name + "_arraylike" | |
550 result = self._JSObject(arraylike_name, recursion_budget - 1) | |
551 length = random.randint(0, 1000) | |
552 result.append("try { %s.length = %d; } catch(e) {}" % | |
553 (arraylike_name, length)) | |
554 return (result + | |
555 self._Variable(name, | |
556 "new %sArray(%s)" % (arraytype, arraylike_name), | |
557 fallback="new %sArray(8)" % arraytype)) | |
558 else: | |
559 die = random.random() | |
560 buffer_name = name + "_buffer" | |
561 args = [buffer_name] | |
562 result = self._JSArrayBuffer(buffer_name, recursion_budget) | |
563 if die < 0.67: | |
564 offset_name = name + "_offset" | |
565 args.append(offset_name) | |
566 result += self._Int32(offset_name) | |
567 if die < 0.33: | |
568 length_name = name + "_length" | |
569 args.append(length_name) | |
570 result += self._Int32(length_name) | |
571 return (result + | |
572 self._Variable(name, | |
573 "new %sArray(%s)" % (arraytype, ", ".join(args)), | |
574 fallback="new %sArray(8)" % arraytype)) | |
575 | |
576 def _JSWeakCollection(self, name, recursion_budget): | |
577 ctor = random.choice([self._JSMap, self._JSSet]) | |
578 return ctor(name, recursion_budget, weak="Weak") | |
579 | |
580 def _PropertyDetails(self, name, recursion_budget): | |
581 # TODO(jkummerow): Be more clever here? | |
582 return self._Int32(name) | |
583 | |
584 def _JSObject(self, name, recursion_budget): | |
585 if random.random() < 0.4: | |
586 function = random.choice([self._PlainObject, self._PlainArray, | |
587 self._PlainFunction]) | |
588 else: | |
589 function = random.choice([self._JSArrayBuffer, self._JSDataView, | |
590 self._JSDate, self._JSFunctionProxy, | |
591 self._JSGeneratorObject, self._JSMap, | |
592 self._JSMapIterator, self._JSRegExp, | |
593 self._JSSet, self._JSSetIterator, | |
594 self._JSTypedArray, self._JSValue, | |
595 self._JSWeakCollection]) | |
596 result = function(name, recursion_budget) | |
597 self._AddAccessors(name, result, recursion_budget) | |
598 self._AddProperties(name, result, recursion_budget) | |
599 self._AddElements(name, result, recursion_budget) | |
600 return result | |
601 | |
602 def _JSReceiver(self, name, recursion_budget): | |
603 if random.random() < 0.9: return self._JSObject(name, recursion_budget) | |
604 return self._JSProxy(name, recursion_budget) | |
605 | |
606 def _HeapObject(self, name, recursion_budget): | |
607 die = random.random() | |
608 if die < 0.9: return self._JSReceiver(name, recursion_budget) | |
609 elif die < 0.95: return self._Oddball(name, recursion_budget) | |
610 else: return self._Name(name, recursion_budget) | |
611 | |
612 def _Object(self, name, recursion_budget): | |
613 if recursion_budget <= 0: | |
614 function = random.choice([self._Oddball, self._Number, self._Name, | |
615 self._JSValue, self._JSRegExp]) | |
616 return function(name, recursion_budget) | |
617 if random.random() < 0.2: | |
618 return self._Smi(name, recursion_budget) | |
619 return self._HeapObject(name, recursion_budget) | |
620 | |
621 GENERATORS = { | |
622 "Boolean": ["true", _Boolean], | |
623 "HeapObject": ["new Object()", _HeapObject], | |
624 "Int32": ["32", _Int32], | |
625 "JSArray": ["new Array()", _JSArray], | |
626 "JSArrayBuffer": ["new ArrayBuffer(8)", _JSArrayBuffer], | |
627 "JSDataView": ["new DataView(new ArrayBuffer(8))", _JSDataView], | |
628 "JSDate": ["new Date()", _JSDate], | |
629 "JSFunction": ["function() {}", _JSFunction], | |
630 "JSFunctionProxy": ["Proxy.createFunction({}, function() {})", | |
631 _JSFunctionProxy], | |
632 "JSGeneratorObject": ["(function*(){ yield 1; })()", _JSGeneratorObject], | |
633 "JSMap": ["new Map()", _JSMap], | |
634 "JSMapIterator": ["%MapCreateIterator(new Map(), 3)", _JSMapIterator], | |
635 "JSObject": ["new Object()", _JSObject], | |
636 "JSProxy": ["Proxy.create({})", _JSProxy], | |
637 "JSReceiver": ["new Object()", _JSReceiver], | |
638 "JSRegExp": ["/ab/g", _JSRegExp], | |
639 "JSSet": ["new Set()", _JSSet], | |
640 "JSSetIterator": ["%SetCreateIterator(new Set(), 2)", _JSSetIterator], | |
641 "JSTypedArray": ["new Int32Array(2)", _JSTypedArray], | |
642 "JSValue": ["new String('foo')", _JSValue], | |
643 "JSWeakCollection": ["new WeakMap()", _JSWeakCollection], | |
644 "Name": ["\"name\"", _Name], | |
645 "Number": ["1.5", _Number], | |
646 "Object": ["new Object()", _Object], | |
647 "PropertyDetails": ["513", _PropertyDetails], | |
648 "SeqString": ["\"seqstring\"", _SeqString], | |
649 "Smi": ["1", _Smi], | |
650 "StrictMode": ["1", _StrictMode], | |
651 "String": ["\"foo\"", _String], | |
652 "Symbol": ["Symbol(\"symbol\")", _Symbol], | |
653 "Uint32": ["32", _Uint32], | |
654 } | |
193 | 655 |
194 | 656 |
195 class ArgParser(object): | 657 class ArgParser(object): |
196 def __init__(self, regex, ctor): | 658 def __init__(self, regex, ctor): |
197 self.regex = regex | 659 self.regex = regex |
198 self.ArgCtor = ctor | 660 self.ArgCtor = ctor |
199 | 661 |
200 | 662 |
201 class Arg(object): | 663 class Arg(object): |
202 def __init__(self, typename, varname, index): | 664 def __init__(self, typename, varname, index): |
(...skipping 44 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
247 | 709 |
248 property_details_parser = ArgParser( | 710 property_details_parser = ArgParser( |
249 re.compile("^\s*CONVERT_PROPERTY_DETAILS_CHECKED\((\w+), (\d+)\)"), | 711 re.compile("^\s*CONVERT_PROPERTY_DETAILS_CHECKED\((\w+), (\d+)\)"), |
250 lambda match: Arg("PropertyDetails", match.group(1), int(match.group(2)))) | 712 lambda match: Arg("PropertyDetails", match.group(1), int(match.group(2)))) |
251 | 713 |
252 arg_parsers = [handle_arg_parser, plain_arg_parser, number_handle_arg_parser, | 714 arg_parsers = [handle_arg_parser, plain_arg_parser, number_handle_arg_parser, |
253 smi_arg_parser, | 715 smi_arg_parser, |
254 double_arg_parser, number_arg_parser, strict_mode_arg_parser, | 716 double_arg_parser, number_arg_parser, strict_mode_arg_parser, |
255 boolean_arg_parser, property_details_parser] | 717 boolean_arg_parser, property_details_parser] |
256 | 718 |
257 | |
258 def SetArgsLength(self, match): | 719 def SetArgsLength(self, match): |
259 self.argslength = int(match.group(1)) | 720 self.argslength = int(match.group(1)) |
260 | 721 |
261 def TryParseArg(self, line): | 722 def TryParseArg(self, line): |
262 for parser in Function.arg_parsers: | 723 for parser in Function.arg_parsers: |
263 match = parser.regex.match(line) | 724 match = parser.regex.match(line) |
264 if match: | 725 if match: |
265 arg = parser.ArgCtor(match) | 726 arg = parser.ArgCtor(match) |
266 self.args[arg.index] = arg | 727 self.args[arg.index] = arg |
267 return True | 728 return True |
(...skipping 10 matching lines...) Expand all Loading... | |
278 if self.args: | 739 if self.args: |
279 argcount = max([self.args[i].index + 1 for i in self.args]) | 740 argcount = max([self.args[i].index + 1 for i in self.args]) |
280 else: | 741 else: |
281 argcount = 0 | 742 argcount = 0 |
282 for i in range(argcount): | 743 for i in range(argcount): |
283 if i > 0: s.append(", ") | 744 if i > 0: s.append(", ") |
284 s.append(self.args[i].type if i in self.args else "<unknown>") | 745 s.append(self.args[i].type if i in self.args else "<unknown>") |
285 s.append(")") | 746 s.append(")") |
286 return "".join(s) | 747 return "".join(s) |
287 | 748 |
749 | |
288 # Parses HEADERFILENAME to find out which runtime functions are "inline". | 750 # Parses HEADERFILENAME to find out which runtime functions are "inline". |
289 def FindInlineRuntimeFunctions(): | 751 def FindInlineRuntimeFunctions(): |
290 inline_functions = [] | 752 inline_functions = [] |
291 with open(HEADERFILENAME, "r") as f: | 753 with open(HEADERFILENAME, "r") as f: |
292 inline_list = "#define INLINE_FUNCTION_LIST(F) \\\n" | 754 inline_list = "#define INLINE_FUNCTION_LIST(F) \\\n" |
293 inline_opt_list = "#define INLINE_OPTIMIZED_FUNCTION_LIST(F) \\\n" | 755 inline_opt_list = "#define INLINE_OPTIMIZED_FUNCTION_LIST(F) \\\n" |
294 inline_function = re.compile(r"^\s*F\((\w+), \d+, \d+\)\s*\\?") | 756 inline_function = re.compile(r"^\s*F\((\w+), \d+, \d+\)\s*\\?") |
295 mode = "SEARCHING" | 757 mode = "SEARCHING" |
296 for line in f: | 758 for line in f: |
297 if mode == "ACTIVE": | 759 if mode == "ACTIVE": |
(...skipping 86 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
384 # All good, there's a custom definition. | 846 # All good, there's a custom definition. |
385 pass | 847 pass |
386 elif not i in f.args: | 848 elif not i in f.args: |
387 # No custom definition and no parse result -> give up. | 849 # No custom definition and no parse result -> give up. |
388 decision = unknown_functions | 850 decision = unknown_functions |
389 else: | 851 else: |
390 t = f.args[i].type | 852 t = f.args[i].type |
391 if t in NON_JS_TYPES: | 853 if t in NON_JS_TYPES: |
392 decision = cctest_fuzzable_functions | 854 decision = cctest_fuzzable_functions |
393 else: | 855 else: |
394 assert t in JS_TYPE_GENERATORS, \ | 856 assert Generator.IsTypeSupported(t), \ |
395 ("type generator not found for %s, function: %s" % (t, f)) | 857 ("type generator not found for %s, function: %s" % (t, f)) |
396 decision.append(f) | 858 decision.append(f) |
397 return (js_fuzzable_functions, cctest_fuzzable_functions, unknown_functions) | 859 return (js_fuzzable_functions, cctest_fuzzable_functions, unknown_functions) |
398 | 860 |
399 | 861 |
400 def GenerateJSTestcaseForFunction(f): | 862 def _GetKnownGoodArgs(function, generator): |
863 custom_input = CUSTOM_KNOWN_GOOD_INPUT.get(function.name, None) | |
864 definitions = [] | |
865 argslist = [] | |
866 for i in range(function.argslength): | |
867 if custom_input and custom_input[i] is not None: | |
868 name = "arg%d" % i | |
869 definitions.append("var %s = %s;" % (name, custom_input[i])) | |
870 else: | |
871 arg = function.args[i] | |
872 name = arg.name | |
873 definitions += generator.RandomVariable(name, arg.type, simple=True) | |
874 argslist.append(name) | |
875 return (definitions, argslist) | |
876 | |
877 | |
878 def _GenerateTestcase(function, definitions, argslist, throws): | |
401 s = ["// Copyright 2014 the V8 project authors. All rights reserved.", | 879 s = ["// Copyright 2014 the V8 project authors. All rights reserved.", |
402 "// AUTO-GENERATED BY tools/generate-runtime-tests.py, DO NOT MODIFY", | 880 "// AUTO-GENERATED BY tools/generate-runtime-tests.py, DO NOT MODIFY", |
403 "// Flags: --allow-natives-syntax --harmony"] | 881 "// Flags: --allow-natives-syntax --harmony"] + definitions |
404 call = "%%%s%s(" % (f.inline, f.name) | 882 call = "%%%s%s(%s);" % (function.inline, function.name, ", ".join(argslist)) |
405 custom = CUSTOM_KNOWN_GOOD_INPUT.get(f.name, None) | 883 if throws: |
406 for i in range(f.argslength): | |
407 if custom and custom[i] is not None: | |
408 (name, value) = ("arg%d" % i, custom[i]) | |
409 else: | |
410 arg = f.args[i] | |
411 (name, value) = (arg.name, JS_TYPE_GENERATORS[arg.type]) | |
412 s.append("var %s = %s;" % (name, value)) | |
413 if i > 0: call += ", " | |
414 call += name | |
415 call += ");" | |
416 if f.name in THROWS: | |
417 s.append("try {") | 884 s.append("try {") |
418 s.append(call); | 885 s.append(call); |
419 s.append("} catch(e) {}") | 886 s.append("} catch(e) {}") |
420 else: | 887 else: |
421 s.append(call) | 888 s.append(call) |
422 testcase = "\n".join(s) | 889 testcase = "\n".join(s) |
423 path = os.path.join(BASEPATH, f.Filename()) | 890 return testcase |
891 | |
892 | |
893 def GenerateJSTestcaseForFunction(function): | |
894 gen = Generator() | |
895 (definitions, argslist) = _GetKnownGoodArgs(function, gen) | |
896 testcase = _GenerateTestcase(function, definitions, argslist, | |
897 function.name in THROWS) | |
898 path = os.path.join(BASEPATH, function.Filename()) | |
424 with open(path, "w") as f: | 899 with open(path, "w") as f: |
425 f.write("%s\n" % testcase) | 900 f.write("%s\n" % testcase) |
426 | 901 |
902 | |
427 def GenerateTestcases(functions): | 903 def GenerateTestcases(functions): |
428 shutil.rmtree(BASEPATH) # Re-generate everything. | 904 shutil.rmtree(BASEPATH) # Re-generate everything. |
429 os.makedirs(BASEPATH) | 905 os.makedirs(BASEPATH) |
430 for f in functions: | 906 for f in functions: |
431 GenerateJSTestcaseForFunction(f) | 907 GenerateJSTestcaseForFunction(f) |
432 | 908 |
433 def PrintUsage(): | 909 |
434 print """Usage: %(this_script)s ACTION | 910 def _SaveFileName(save_path, process_id, save_file_index): |
911 return "%s/fuzz_%d_%d.js" % (save_path, process_id, save_file_index) | |
912 | |
913 | |
914 def RunFuzzer(process_id, options, stop_running): | |
915 base_file_name = "/dev/shm/runtime_fuzz_%d" % process_id | |
916 test_file_name = "%s.js" % base_file_name | |
917 stderr_file_name = "%s.out" % base_file_name | |
918 save_file_index = 0 | |
919 while os.path.exists(_SaveFileName(options.save_path, process_id, | |
920 save_file_index)): | |
921 save_file_index += 1 | |
922 MAX_SLEEP_TIME = 0.1 | |
923 INITIAL_SLEEP_TIME = 0.001 | |
924 SLEEP_TIME_FACTOR = 1.5 | |
925 | |
926 functions = FindRuntimeFunctions() | |
927 (js_fuzzable_functions, cctest_fuzzable_functions, unknown_functions) = \ | |
928 ClassifyFunctions(functions) | |
929 | |
930 try: | |
931 for i in range(options.num_tests): | |
932 if stop_running.is_set(): break | |
933 function = random.choice(js_fuzzable_functions) # TODO: others too | |
934 if function.argslength == 0: continue | |
935 args = [] | |
936 definitions = [] | |
937 gen = Generator() | |
938 for i in range(function.argslength): | |
939 arg = function.args[i] | |
940 argname = "arg%d%s" % (i, arg.name) | |
941 args.append(argname) | |
942 definitions += gen.RandomVariable(argname, arg.type, simple=False) | |
943 testcase = _GenerateTestcase(function, definitions, args, True) | |
944 with open(test_file_name, "w") as f: | |
945 f.write("%s\n" % testcase) | |
946 with open("/dev/null", "w") as devnull: | |
947 with open(stderr_file_name, "w") as stderr: | |
948 process = subprocess.Popen( | |
949 [options.binary, "--allow-natives-syntax", "--harmony", | |
950 "--enable-slow-asserts", test_file_name], | |
951 stdout=devnull, stderr=stderr) | |
952 end_time = time.time() + options.timeout | |
953 timed_out = False | |
954 exit_code = None | |
955 sleep_time = INITIAL_SLEEP_TIME | |
956 while exit_code is None: | |
957 if time.time() >= end_time: | |
958 # Kill the process and wait for it to exit. | |
959 os.kill(process.pid, signal.SIGTERM) | |
960 exit_code = process.wait() | |
961 timed_out = True | |
962 else: | |
963 exit_code = process.poll() | |
964 time.sleep(sleep_time) | |
965 sleep_time = sleep_time * SLEEP_TIME_FACTOR | |
966 if sleep_time > MAX_SLEEP_TIME: | |
967 sleep_time = MAX_SLEEP_TIME | |
968 if exit_code != 0 and not timed_out: | |
969 oom = False | |
970 with open(stderr_file_name, "r") as stderr: | |
971 for line in stderr: | |
972 if line.strip() == "# Allocation failed - process out of memory": | |
973 oom = True | |
974 break | |
975 if oom: continue | |
976 save_name = _SaveFileName(options.save_path, process_id, | |
977 save_file_index) | |
978 shutil.copyfile(test_file_name, save_name) | |
979 save_file_index += 1 | |
980 except KeyboardInterrupt: | |
981 stop_running.set() | |
982 except Exception, e: | |
983 print e | |
984 finally: | |
985 os.remove(test_file_name) | |
986 os.remove(stderr_file_name) | |
987 | |
988 | |
989 def BuildOptionParser(): | |
990 usage = """Usage: %%prog [options] ACTION | |
435 | 991 |
436 where ACTION can be: | 992 where ACTION can be: |
437 | 993 |
438 info Print diagnostic info. | 994 info Print diagnostic info. |
439 check Check that runtime functions can be parsed as expected, and that | 995 check Check that runtime functions can be parsed as expected, and that |
440 test cases exist. | 996 test cases exist. |
441 generate Parse source code for runtime functions, and auto-generate | 997 generate Parse source code for runtime functions, and auto-generate |
442 test cases for them. Warning: this will nuke and re-create | 998 test cases for them. Warning: this will nuke and re-create |
443 %(path)s. | 999 %(path)s. |
444 """ % {"path": os.path.relpath(BASEPATH), "this_script": THIS_SCRIPT} | 1000 fuzz Generate fuzz tests, run them, save those that crashed (see options). |
1001 """ % {"path": os.path.relpath(BASEPATH)} | |
445 | 1002 |
446 if __name__ == "__main__": | 1003 o = optparse.OptionParser(usage=usage) |
447 if len(sys.argv) != 2: | 1004 o.add_option("--binary", default="out/x64.debug/d8", |
448 PrintUsage() | 1005 help="d8 binary used for running fuzz tests (default: %default)") |
449 sys.exit(1) | 1006 o.add_option("-n", "--num-tests", default=1000, type="int", |
450 action = sys.argv[1] | 1007 help="Number of fuzz tests to generate per worker process" |
451 if action in ["-h", "--help", "help"]: | 1008 " (default: %default)") |
452 PrintUsage() | 1009 o.add_option("--save-path", default="~/runtime_fuzz_output", |
453 sys.exit(0) | 1010 help="Path to directory where failing tests will be stored" |
1011 " (default: %default)") | |
1012 o.add_option("--timeout", default=20, type="int", | |
1013 help="Timeout for each fuzz test (in seconds, default:" | |
1014 "%default)") | |
1015 return o | |
1016 | |
1017 | |
1018 def Main(): | |
1019 parser = BuildOptionParser() | |
1020 (options, args) = parser.parse_args() | |
1021 options.save_path = os.path.expanduser(options.save_path) | |
1022 | |
1023 if len(args) != 1 or args[0] == "help": | |
1024 parser.print_help() | |
1025 return 1 | |
1026 action = args[0] | |
454 | 1027 |
455 functions = FindRuntimeFunctions() | 1028 functions = FindRuntimeFunctions() |
456 (js_fuzzable_functions, cctest_fuzzable_functions, unknown_functions) = \ | 1029 (js_fuzzable_functions, cctest_fuzzable_functions, unknown_functions) = \ |
457 ClassifyFunctions(functions) | 1030 ClassifyFunctions(functions) |
458 | 1031 |
1032 if action == "test": | |
1033 gen = Generator() | |
1034 vartype = "JSTypedArray" | |
1035 print("simple: %s" % gen.RandomVariable("x", vartype, True)) | |
1036 for i in range(10): | |
1037 print("----") | |
1038 print("%s" % "\n".join(gen.RandomVariable("x", vartype, False))) | |
1039 return 0 | |
1040 | |
459 if action == "info": | 1041 if action == "info": |
460 print("%d functions total; js_fuzzable_functions: %d, " | 1042 print("%d functions total; js_fuzzable_functions: %d, " |
461 "cctest_fuzzable_functions: %d, unknown_functions: %d" | 1043 "cctest_fuzzable_functions: %d, unknown_functions: %d" |
462 % (len(functions), len(js_fuzzable_functions), | 1044 % (len(functions), len(js_fuzzable_functions), |
463 len(cctest_fuzzable_functions), len(unknown_functions))) | 1045 len(cctest_fuzzable_functions), len(unknown_functions))) |
464 print("unknown functions:") | 1046 print("unknown functions:") |
465 for f in unknown_functions: | 1047 for f in unknown_functions: |
466 print(f) | 1048 print(f) |
467 sys.exit(0) | 1049 return 0 |
468 | 1050 |
469 if action == "check": | 1051 if action == "check": |
470 error = False | 1052 errors = 0 |
1053 | |
471 def CheckCount(actual, expected, description): | 1054 def CheckCount(actual, expected, description): |
472 global error | |
473 if len(actual) != expected: | 1055 if len(actual) != expected: |
474 print("Expected to detect %d %s, but found %d." % ( | 1056 print("Expected to detect %d %s, but found %d." % ( |
475 expected, description, len(actual))) | 1057 expected, description, len(actual))) |
476 print("If this change is intentional, please update the expectations" | 1058 print("If this change is intentional, please update the expectations" |
477 " at the top of %s." % THIS_SCRIPT) | 1059 " at the top of %s." % THIS_SCRIPT) |
478 error = True | 1060 return 1 |
479 CheckCount(functions, EXPECTED_FUNCTION_COUNT, "functions in total") | 1061 return 0 |
480 CheckCount(js_fuzzable_functions, EXPECTED_FUZZABLE_COUNT, | 1062 |
481 "JavaScript-fuzzable functions") | 1063 errors += CheckCount(functions, EXPECTED_FUNCTION_COUNT, |
482 CheckCount(cctest_fuzzable_functions, EXPECTED_CCTEST_COUNT, | 1064 "functions in total") |
483 "cctest-fuzzable functions") | 1065 errors += CheckCount(js_fuzzable_functions, EXPECTED_FUZZABLE_COUNT, |
484 CheckCount(unknown_functions, EXPECTED_UNKNOWN_COUNT, | 1066 "JavaScript-fuzzable functions") |
485 "functions with incomplete type information") | 1067 errors += CheckCount(cctest_fuzzable_functions, EXPECTED_CCTEST_COUNT, |
1068 "cctest-fuzzable functions") | |
1069 errors += CheckCount(unknown_functions, EXPECTED_UNKNOWN_COUNT, | |
1070 "functions with incomplete type information") | |
486 | 1071 |
487 def CheckTestcasesExisting(functions): | 1072 def CheckTestcasesExisting(functions): |
488 global error | 1073 errors = 0 |
489 for f in functions: | 1074 for f in functions: |
490 if not os.path.isfile(os.path.join(BASEPATH, f.Filename())): | 1075 if not os.path.isfile(os.path.join(BASEPATH, f.Filename())): |
491 print("Missing testcase for %s, please run '%s generate'" % | 1076 print("Missing testcase for %s, please run '%s generate'" % |
492 (f.name, THIS_SCRIPT)) | 1077 (f.name, THIS_SCRIPT)) |
493 error = True | 1078 errors += 1 |
494 files = filter(lambda filename: not filename.startswith("."), | 1079 files = filter(lambda filename: not filename.startswith("."), |
495 os.listdir(BASEPATH)) | 1080 os.listdir(BASEPATH)) |
496 if (len(files) != len(functions)): | 1081 if (len(files) != len(functions)): |
497 unexpected_files = set(files) - set([f.Filename() for f in functions]) | 1082 unexpected_files = set(files) - set([f.Filename() for f in functions]) |
498 for f in unexpected_files: | 1083 for f in unexpected_files: |
499 print("Unexpected testcase: %s" % os.path.join(BASEPATH, f)) | 1084 print("Unexpected testcase: %s" % os.path.join(BASEPATH, f)) |
500 error = True | 1085 errors += 1 |
501 print("Run '%s generate' to automatically clean these up." | 1086 print("Run '%s generate' to automatically clean these up." |
502 % THIS_SCRIPT) | 1087 % THIS_SCRIPT) |
503 CheckTestcasesExisting(js_fuzzable_functions) | 1088 return errors |
504 | 1089 |
505 if error: | 1090 errors += CheckTestcasesExisting(js_fuzzable_functions) |
506 sys.exit(1) | 1091 |
1092 if errors > 0: | |
1093 return 1 | |
507 print("Generated runtime tests: all good.") | 1094 print("Generated runtime tests: all good.") |
508 sys.exit(0) | 1095 return 0 |
509 | 1096 |
510 if action == "generate": | 1097 if action == "generate": |
511 GenerateTestcases(js_fuzzable_functions) | 1098 GenerateTestcases(js_fuzzable_functions) |
512 sys.exit(0) | 1099 return 0 |
1100 | |
1101 if action == "fuzz": | |
1102 processes = [] | |
1103 if not os.path.isdir(options.save_path): | |
1104 os.makedirs(options.save_path) | |
1105 stop_running = multiprocessing.Event() | |
1106 for i in range(multiprocessing.cpu_count()): | |
1107 args = (i, options, stop_running) | |
1108 p = multiprocessing.Process(target=RunFuzzer, args=args) | |
1109 p.start() | |
1110 processes.append(p) | |
1111 try: | |
1112 for i in range(len(processes)): | |
1113 processes[i].join() | |
1114 except KeyboardInterrupt: | |
1115 stop_running.set() | |
1116 for i in range(len(processes)): | |
1117 processes[i].join() | |
1118 return 0 | |
1119 | |
1120 if __name__ == "__main__": | |
1121 sys.exit(Main()) | |
OLD | NEW |