diff --git a/tests/test_engine.py b/tests/test_engine.py new file mode 100644 index 0000000..6e3effc --- /dev/null +++ b/tests/test_engine.py @@ -0,0 +1,154 @@ +from __future__ import annotations + +import ast +import json +import textwrap +from pathlib import Path + +import pytest + +from foreignthon.transpiler import _check_shebang, _detect_lang, detranspile, transpile + +# --------------------------------------------------------------------------- +# Setup: Load the local test JSON fixture +# --------------------------------------------------------------------------- + +TEST_PACK_PATH = Path(__file__).parent / "test_pack.json" +TEST_PACK = json.loads(TEST_PACK_PATH.read_text(encoding="utf-8")) + + +def core_transpile(src: str) -> str: + # We pass "es" to match the JSON's meta code, but we feed it the local pack + return transpile(textwrap.dedent(src).strip() + "\n", "es", pack=TEST_PACK) + + +def core_detranspile(src: str, postfix: bool = False) -> str: + return detranspile( + textwrap.dedent(src).strip() + "\n", "es", postfix=postfix, pack=TEST_PACK + ) + + +def valid(src: str) -> bool: + try: + ast.parse(src) + return True + except SyntaxError: + return False + + +# --------------------------------------------------------------------------- +# 1. Core Mechanics: Translation & AST Validity +# --------------------------------------------------------------------------- + + +def test_engine_basic_translation(): + # Ensures the engine reads the dictionary and swaps the words + src = """ + para i en dist(5): + escribir(i) + """ + out = core_transpile(src) + assert "for" in out and "in" in out and "range" in out and "print" in out + assert valid(out) + + +# --------------------------------------------------------------------------- +# 2. Core Mechanics: Safety Boundaries (Strings & Comments) +# --------------------------------------------------------------------------- + + +def test_engine_preserves_strings(): + # The engine MUST NOT translate keywords hidden inside strings + out = core_transpile('mensaje = "si para mientras def clase"') + assert '"si para mientras def clase"' in out + + +def test_engine_preserves_comments(): + # The engine MUST NOT translate keywords hidden in comments + out = core_transpile("# si para mientras\nx = 1") + assert "# si para mientras" in out + + +def test_engine_preserves_fstrings(): + out = core_transpile('escribir(f"valor si={42}")') + assert "si" in out # 'si' survives because it is inside the string + + +# --------------------------------------------------------------------------- +# 3. Core Mechanics: Postfix Syntax (@@) +# --------------------------------------------------------------------------- + + +def test_engine_postfix_reversal(): + # Tests the engine's ability to move the keyword to the front + src = """ + x = 5 + x > 0 @@si: + escribir(x) + """ + out = core_transpile(src) + assert "if x > 0:" in out + assert "@@" not in out + assert valid(out) + + +def test_engine_mixed_prefix_postfix(): + src = """ + si x > 0: + escribir(x) + y < 0 @@si: + escribir(y) + """ + out = core_transpile(src) + assert out.count("if") == 2 + assert "@@" not in out + + +# --------------------------------------------------------------------------- +# 4. Core Mechanics: Decompilation (Round Trip) +# --------------------------------------------------------------------------- + + +def test_engine_detranspile(): + # Standard Python should turn back into foreignthon syntax + src = """ + if x > 0: + pass + """ + out = core_detranspile(src) + assert "si" in out and "pasar" in out + + +def test_engine_roundtrip(): + # foreignthon -> Python -> foreignthon + original = "para i en dist(5):\n escribir(i)\n" + compiled = core_transpile(original) + assert valid(compiled) + + back = core_detranspile(compiled) + assert "para" in back and "dist" in back + # Accept either valid translation for 'print' + assert "escribir" in back or "imprimir" in back + + +# --------------------------------------------------------------------------- +# 5. Core Utilities: Detection & Shebangs +# --------------------------------------------------------------------------- + + +def test_detect_lang_from_extension(): + assert _detect_lang(Path("script.es.py")) == "es" + assert _detect_lang(Path("script.ta.py")) == "ta" + + +def test_detect_lang_bad_extension(): + with pytest.raises(ValueError): + _detect_lang(Path("script.py")) + + +def test_shebang_override(): + assert _check_shebang("# foreignthon: fr\nsi x:\n pasar", "es") == "fr" + + +def test_shebang_default_when_absent(): + assert _check_shebang("si x:\n pasar", "es") == "es" diff --git a/tests/test_pack.json b/tests/test_pack.json new file mode 100644 index 0000000..098db87 --- /dev/null +++ b/tests/test_pack.json @@ -0,0 +1,147 @@ +{ + "meta": { + "name": "Spanish", + "native_name": "Español", + "code": "es" + }, + "keywords": { + "si": "if", + "sino": "else", + "osi": "elif", + "para": "for", + "mientras": "while", + "def": "def", + "clase": "class", + "importar": "import", + "de": "from", + "como": "as", + "retornar": "return", + "parar": "break", + "continuar": "continue", + "pasar": "pass", + "intentar": "try", + "excepto": "except", + "finalmente": "finally", + "lanzar": "raise", + "con": "with", + "en": "in", + "es": "is", + "y": "and", + "o": "or", + "no": "not", + "elim": "del", + "global": "global", + "nolocal": "nonlocal", + "afirmar": "assert", + "generar": "yield", + "esperar": "await", + "asinc": "async", + "lambda": "lambda", + "Verda": "True", + "Falso": "False", + "Nada": "None" + }, + "builtins": { + "escribir": "print", + "imprimir": "print", + "entrada": "input", + "lon": "len", + "dist": "range", + "tipo": "type", + "ent": "int", + "dec": "float", + "texto": "str", + "lista": "list", + "dicc": "dict", + "conj": "set", + "tupla": "tuple", + "bool": "bool", + "abrir": "open", + "enumerar": "enumerate", + "map": "map", + "filtrar": "filter", + "ordenado": "sorted", + "invertido": "reversed", + "sum": "sum", + "min": "min", + "max": "max", + "abs": "abs", + "redondear": "round", + "rnd": "round", + "todos": "all", + "alguno": "any", + "esinstancia": "isinstance", + "teneatri": "hasattr", + "obtatri": "getattr", + "estabatri": "setattr", + "repr": "repr", + "formatear": "format", + "vars": "vars", + "sigue": "next", + "id": "id", + "car": "chr", + "hex": "hex", + "bin": "bin", + "oct": "oct" + }, + "exceptions": { + "Excepcion": "Exception", + "ExcepcionBase": "BaseException", + "ErrorDeValor": "ValueError", + "ErrorDeTipo": "TypeError", + "ErrorDeClave": "KeyError", + "ErrorDeIndice": "IndexError", + "ErrorDeAtributo": "AttributeError", + "ErrorDeNombre": "NameError", + "ErrorDeImportacion": "ImportError", + "ErrorDelSistema": "OSError", + "ArchivoNoEncontrado": "FileNotFoundError", + "ErrorDeEjecucion": "RuntimeError", + "DetenerIteracion": "StopIteration", + "SalidaDelSistema": "SystemExit", + "InterrupcionDeTeclado": "KeyboardInterrupt", + "ErrorNoImplementado": "NotImplementedError", + "ErrorDeDivisionCero": "ZeroDivisionError", + "ErrorDeRecursion": "RecursionError", + "ErrorDeSintaxis": "SyntaxError", + "ErrorDeAfirmacion": "AssertionError", + "ErrorDeDesbordamiento": "OverflowError", + "ErrorDeMemoria": "MemoryError", + "ErrorDePermiso": "PermissionError", + "ErrorDeTiempoAgotado": "TimeoutError" + }, + "error_messages": { + "SyntaxError": "Error de sintaxis", + "ValueError": "Error de valor", + "TypeError": "Error de tipo", + "KeyError": "Error de clave", + "IndexError": "Error de índice", + "AttributeError": "Error de atributo", + "NameError": "Error de nombre", + "ImportError": "Error de importación", + "FileNotFoundError": "Archivo no encontrado", + "ZeroDivisionError": "Error división por cero", + "RecursionError": "Error de recursión", + "RuntimeError": "Error de ejecución", + "MemoryError": "Error de memoria", + "OverflowError": "Error de desbordamiento", + "AssertionError": "Error de afirmación", + "NotImplementedError": "Error no implementado", + "StopIteration": "Detener iteración", + "KeyboardInterrupt": "Interrupción de teclado", + "PermissionError": "Error de permiso", + "TimeoutError": "Error de tiempo agotado" + }, + "stdlib": { + "mate": "math", + "sis": "sys", + "fechahora": "datetime", + "tiempo": "time", + "aleatorio": "random", + "aleatoria": "random", + "colecciones": "collections", + "ruta": "pathlib", + "er": "re" + }, + "postfix_keywords": [] +}