diff --git a/api/utils.py b/api/utils.py index 04f317c29..45dd3c4ae 100644 --- a/api/utils.py +++ b/api/utils.py @@ -2,7 +2,10 @@ # -*- coding: utf-8 -*- import os +import errno import shelve +import signal +from functools import wraps from typing import NamedTuple, List, Any from . import constants @@ -13,7 +16,13 @@ import symbols -__all__ = ['read_txt_file', 'open_file', 'sanitize_filename', 'flatten_list'] +__all__ = [ + 'read_txt_file', + 'open_file', + 'sanitize_filename', + 'flatten_list', + 'timeout' +] __doc__ = """Utils module contains many helpers for several task, like reading files or path management""" @@ -136,3 +145,22 @@ def get_final_value(symbol: symbols.SYMBOL): result = result.value return result + + +def timeout(seconds=10, error_message=os.strerror(errno.ETIME)): + def decorator(func): + def _handle_timeout(signum, frame): + raise TimeoutError(error_message) + + def wrapper(*args, **kwargs): + signal.signal(signal.SIGALRM, _handle_timeout) + signal.alarm(seconds) + try: + result = func(*args, **kwargs) + finally: + signal.alarm(0) + return result + + return wraps(func)(wrapper) + + return decorator diff --git a/ast_/ast.py b/ast_/ast.py index 5389e8a02..3a63210af 100644 --- a/ast_/ast.py +++ b/ast_/ast.py @@ -57,9 +57,13 @@ def generic_visit(node: Ast): def filter_inorder(self, node, filter_func: Callable[[Any], bool]): """ Visit the tree inorder, but only those that return true for filter """ + visited = set() stack = [node] while stack: node = stack.pop() + if node in visited: + continue + visited.add(node) if filter_func(node): yield self.visit(node) elif isinstance(node, Ast): diff --git a/tests/functional/opt2_infinite_loop.asm b/tests/functional/opt2_infinite_loop.asm new file mode 100644 index 000000000..82fb7b2a3 --- /dev/null +++ b/tests/functional/opt2_infinite_loop.asm @@ -0,0 +1,46 @@ + org 32768 +__START_PROGRAM: + di + push ix + push iy + exx + push hl + exx + ld hl, 0 + add hl, sp + ld (__CALL_BACK__), hl + ei + jp __MAIN_PROGRAM__ +ZXBASIC_USER_DATA: + ; Defines USER DATA Length in bytes +ZXBASIC_USER_DATA_LEN EQU ZXBASIC_USER_DATA_END - ZXBASIC_USER_DATA + .__LABEL__.ZXBASIC_USER_DATA_LEN EQU ZXBASIC_USER_DATA_LEN + .__LABEL__.ZXBASIC_USER_DATA EQU ZXBASIC_USER_DATA +_kill: + DEFB 00 +ZXBASIC_USER_DATA_END: +__MAIN_PROGRAM__: + ld hl, 0 + ld b, h + ld c, l +__END_PROGRAM: + di + ld hl, (__CALL_BACK__) + ld sp, hl + exx + pop hl + pop iy + pop ix + exx + ei + ret +__CALL_BACK__: + DEFW 0 +_bulletcuchillo: + ld a, (_kill) + or a + jp nz, _bulletcuchillo__leave +__LABEL__endbulletcuchillo: +_bulletcuchillo__leave: + ret + END diff --git a/tests/functional/opt2_infinite_loop.bas b/tests/functional/opt2_infinite_loop.bas new file mode 100644 index 000000000..be30fd3fa --- /dev/null +++ b/tests/functional/opt2_infinite_loop.bas @@ -0,0 +1,14 @@ +REM This was causing an infinite loop. + +DIM kill as Ubyte + +sub fastcall bulletcuchillo() + if kill then + return + end if +endbulletcuchillo: +end sub + +DIM dummy as Uinteger +dummy = @bulletcuchillo + diff --git a/tests/functional/test.py b/tests/functional/test.py index b7f563773..59b8f26b0 100755 --- a/tests/functional/test.py +++ b/tests/functional/test.py @@ -30,6 +30,7 @@ sys.path.append(ZXBASIC_ROOT) # TODO: consider moving test.py to another place to avoid this # Now we can import the modules from the root +import api.utils # noqa import libzxbc # noqa import libzxbasm # noqa import libzxbpp # noqa @@ -46,6 +47,7 @@ STDERR = None INLINE = True # Set to false to use system Shell RAISE_EXCEPTIONS = False # True if we want the testing to abort on compiler crashes +TIMEOUT = 3 # Max number of seconds a test should last class TempTestFile(object): @@ -239,6 +241,7 @@ def updateTest(tfname, pattern_): f.write(''.join(lines)) +@api.utils.timeout(TIMEOUT) def testPREPRO(fname, pattern_=None, inline=None, cmdline_args=None): """ Test preprocessing file. Test is done by preprocessing the file and then comparing the output against an expected one. The output file can optionally be filtered @@ -297,6 +300,7 @@ def testPREPRO(fname, pattern_=None, inline=None, cmdline_args=None): return result +@api.utils.timeout(TIMEOUT) def testASM(fname, inline=None, cmdline_args=None): """ Test assembling an ASM (.asm) file. Test is done by assembling the source code into a binary and then comparing the output file against an expected binary output. @@ -339,6 +343,7 @@ def testASM(fname, inline=None, cmdline_args=None): return result +@api.utils.timeout(TIMEOUT) def testBAS(fname, filter_=None, inline=None, cmdline_args=None): """ Test compiling a BASIC (.bas) file. Test is done by compiling the source code into asm and then comparing the output asm against an expected asm output. The output asm file can optionally be filtered