|
| 1 | +import json |
| 2 | +from pathlib import Path |
| 3 | +import subprocess |
| 4 | +import sys |
| 5 | +import tempfile |
| 6 | +import unittest |
| 7 | + |
| 8 | +from JavaScript.emitter import compile_source |
| 9 | +from JavaScript.modules import SourceModule, compile_modules |
| 10 | +from test_compiler_async import run_async |
| 11 | +from test_compiler_emitter import run_node |
| 12 | + |
| 13 | + |
| 14 | +class CompilerErrorOriginTests(unittest.TestCase): |
| 15 | + def assert_origin(self, source, *, asynchronous=False): |
| 16 | + oracle = '''import contextlib,io,json,traceback |
| 17 | +source = ''' + repr(source) + ''' |
| 18 | +namespace={};output=io.StringIO() |
| 19 | +try: |
| 20 | + with contextlib.redirect_stdout(output): |
| 21 | + exec(compile(source,'origin.py','exec'),namespace) |
| 22 | +''' |
| 23 | + if asynchronous: |
| 24 | + oracle += " import asyncio\n asyncio.run(namespace['main']())\n" |
| 25 | + oracle += '''except Exception as error: |
| 26 | + frame=[frame for frame in traceback.extract_tb(error.__traceback__) if frame.filename=='origin.py'][-1] |
| 27 | + print(json.dumps({'stdout':output.getvalue(),'name':type(error).__name__,'message':str(error),'line':frame.lineno})) |
| 28 | +''' |
| 29 | + expected = json.loads(subprocess.check_output([sys.executable, '-I', '-X', 'utf8', '-c', oracle], text=True, encoding='utf-8', timeout=10)) |
| 30 | + artifact = compile_source(source, filename='origin.py', async_enabled=asynchronous) |
| 31 | + actual = run_async(artifact) if asynchronous else run_node(artifact) |
| 32 | + self.assertEqual(actual['stdout'], expected['stdout']) |
| 33 | + self.assertEqual(actual['error']['name'], expected['name']) |
| 34 | + self.assertEqual(actual['error']['message'], expected['message']) |
| 35 | + self.assertEqual(actual['error']['location']['filename'], 'origin.py') |
| 36 | + self.assertEqual(actual['error']['location']['line'], expected['line']) |
| 37 | + return actual |
| 38 | + |
| 39 | + def test_preconstructed_exception_reports_raise_instead_of_constructor(self): |
| 40 | + actual = self.assert_origin('def selected():\n error=TypeError("bad")\n print(error is error)\n raise error\nselected()') |
| 41 | + self.assertEqual(actual['error']['location']['line'], 4) |
| 42 | + self.assertEqual(actual['error']['location']['column'], 5) |
| 43 | + |
| 44 | + def test_promise_value_origin_and_prior_callback_runtime_origins(self): |
| 45 | + cases = [ |
| 46 | + 'from JavaScript import Promise\nasync def main():\n error=TypeError("rejected")\n pending=Promise.reject(error)\n await pending', |
| 47 | + 'from JavaScript import Promise\ndef callback(value):\n print("once")\n error=TypeError("callback")\n raise error\nasync def main():\n await Promise.resolve(1).then(callback)', |
| 48 | + 'async def inner():\n return 1//0\nasync def main():\n return await inner()', |
| 49 | + 'from JavaScript import Promise\ndef identity(value):\n return value\nasync def main():\n error=TypeError("same object")\n observed=await Promise.reject(error).then(None,identity)\n print(error is observed)\n raise error', |
| 50 | + ] |
| 51 | + for source in cases: |
| 52 | + with self.subTest(source=source): |
| 53 | + self.assert_origin(source, asynchronous=True) |
| 54 | + |
| 55 | + def test_shared_runtime_retains_callback_module_origin(self): |
| 56 | + sources = (SourceModule.from_text('app', 'from JavaScript import Promise\nfrom helper import callback\nasync def main():\n await Promise.resolve(1).then(callback)'), |
| 57 | + SourceModule.from_text('helper', 'def callback(value):\n error=TypeError("module")\n raise error')) |
| 58 | + result = run_async(compile_modules(sources, entrypoint='app', async_enabled=True)) |
| 59 | + self.assertEqual(result['error']['location']['filename'], 'helper.py') |
| 60 | + self.assertEqual(result['error']['location']['line'], 3) |
| 61 | + |
| 62 | + def test_native_traceback_maps_to_the_raise_statement(self): |
| 63 | + source = 'def selected():\n error=TypeError("bad")\n raise error\nselected()' |
| 64 | + artifact = compile_source(source, filename='origin.py') |
| 65 | + runner = '''import {createModule} from './app.mjs';import {SourceMap} from 'node:module';import {readFile} from 'node:fs/promises'; |
| 66 | +const map=new SourceMap(JSON.parse(await readFile(new URL('./app.mjs.map',import.meta.url)))); |
| 67 | +try{createModule()}catch(error){const match=error.stack.match(/app\\.mjs:(\\d+):(\\d+)/);console.log(JSON.stringify(map.findEntry(Number(match[1])-1,Number(match[2])-1)));} |
| 68 | +''' |
| 69 | + with tempfile.TemporaryDirectory() as temporary: |
| 70 | + root = Path(temporary) |
| 71 | + for item in artifact.files: |
| 72 | + (root / item.path).write_bytes(item.data) |
| 73 | + (root / 'run.mjs').write_text(runner) |
| 74 | + result = json.loads(subprocess.check_output(['node', str(root / 'run.mjs')], text=True, timeout=10)) |
| 75 | + self.assertEqual(result['originalSource'], 'origin.py') |
| 76 | + self.assertEqual(result['originalLine'], 2) |
| 77 | + self.assertEqual(result['originalColumn'], 4) |
0 commit comments