Skip to content
This repository has been archived by the owner on Sep 12, 2024. It is now read-only.

Commit

Permalink
Merge pull request #548 from Jaseci-Labs/thakee-traceback-dump
Browse files Browse the repository at this point in the history
Dumping traceback of runtime errors impl
  • Loading branch information
marsninja authored Aug 5, 2024
2 parents 9ae0149 + 8585d61 commit c7d715d
Show file tree
Hide file tree
Showing 5 changed files with 102 additions and 4 deletions.
10 changes: 6 additions & 4 deletions jaclang/runtimelib/importer.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@

from jaclang.runtimelib.machine import JacMachine
from jaclang.runtimelib.utils import sys_path_context
from jaclang.utils.helpers import dump_traceback
from jaclang.utils.log import logging

logger = logging.getLogger(__name__)
Expand Down Expand Up @@ -155,9 +156,10 @@ def load_jac_mod_as_item(
exec(codeobj, new_module.__dict__)
return getattr(new_module, name, new_module)
except ImportError as e:
logger.error(
f"Failed to load {name} from {jac_file_path} in {module.__name__}: {str(e)}"
)
logger.error(dump_traceback(e))
# logger.error(
# f"Failed to load {name} from {jac_file_path} in {module.__name__}: {str(e)}"
# )
return None


Expand Down Expand Up @@ -346,7 +348,7 @@ def run_import(
try:
exec(codeobj, module.__dict__)
except Exception as e:
logger.error(f"Error while importing {spec.full_target}: {e}")
logger.error(dump_traceback(e))
raise e
import_return = ImportReturn(module, unique_loaded_items, self)
if spec.items:
Expand Down
15 changes: 15 additions & 0 deletions jaclang/tests/fixtures/err_runtime.jac
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@

can bar(some_list:list) {
invalid_index = 4;
print(some_list[invalid_index]); # This should fail.
}


can foo() {
bar([0, 1, 2, 3]);
}


with entry {
foo();
}
27 changes: 27 additions & 0 deletions jaclang/tests/test_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,33 @@ def test_jac_cli_alert_based_err(self) -> None:
# print(stdout_value)
self.assertIn("Error", stdout_value)

def test_jac_cli_alert_based_runtime_err(self) -> None:
"""Basic test for pass."""
captured_output = io.StringIO()
sys.stdout = captured_output
sys.stderr = captured_output

try:
cli.run(self.fixture_abs_path("err_runtime.jac"))
except Exception as e:
print(f"Error: {e}")

sys.stdout = sys.__stdout__
sys.stderr = sys.__stderr__

expected_stdout_values = (
"Error: list index out of range",
" print(some_list[invalid_index]);",
" ^^^^^^^^^^^^^^^^^^^^^^^^",
" at bar() ",
" at foo() ",
" at <module> ",
)

logger_capture = "\n".join([rec.message for rec in self.caplog.records])
for exp in expected_stdout_values:
self.assertIn(exp, logger_capture)

def test_jac_impl_err(self) -> None:
"""Basic test for pass."""
if "jaclang.tests.fixtures.err" in sys.modules:
Expand Down
45 changes: 45 additions & 0 deletions jaclang/utils/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import os
import pdb
import re
from traceback import TracebackException


def pascal_to_snake(pascal_string: str) -> str:
Expand Down Expand Up @@ -137,6 +138,50 @@ def is_standard_lib_module(module_path: str) -> bool:
return os.path.isfile(file_path) or os.path.isdir(direc_path)


def dump_traceback(e: Exception) -> str:
"""Dump the stack frames of the exception."""
trace_dump = ""

# Utility function to get the error line char offset.
def byte_offset_to_char_offset(string: str, offset: int) -> int:
return len(string.encode("utf-8")[:offset].decode("utf-8", errors="replace"))

tb = TracebackException(type(e), e, e.__traceback__, limit=None, compact=True)
trace_dump += f"Error: {str(e)}"

# The first frame is the call the to the above `exec` function, not usefull to the enduser,
# and Make the most recent call first.
tb.stack.pop(0)
tb.stack.reverse()

# FIXME: should be some settings, we should replace to ensure the anchors length match.
dump_tab_width = 4

for idx, frame in enumerate(tb.stack):
func_signature = frame.name + ("()" if frame.name.isidentifier() else "")

# Pretty print the most recent call's location.
if idx == 0 and (frame.line and frame.line.strip() != ""):
line_o = frame._original_line.rstrip() # type: ignore [attr-defined]
line_s = frame.line.rstrip() if frame.line else ""
stripped_chars = len(line_o) - len(line_s)
trace_dump += f'\n{" " * (dump_tab_width * 2)}{line_s}'
if frame.colno is not None and frame.end_colno is not None:
off_start = byte_offset_to_char_offset(line_o, frame.colno)
off_end = byte_offset_to_char_offset(line_o, frame.end_colno)

# A bunch of caret '^' characters under the error location.
anchors = (" " * (off_start - stripped_chars - 1)) + "^" * len(
line_o[off_start:off_end].replace("\t", " " * dump_tab_width)
)

trace_dump += f'\n{" " * (dump_tab_width * 2)}{anchors}'

trace_dump += f'\n{" " * dump_tab_width}at {func_signature} {frame.filename}:{frame.lineno}'

return trace_dump


class Jdb(pdb.Pdb):
"""Jac debugger."""

Expand Down
9 changes: 9 additions & 0 deletions jaclang/utils/test.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,24 @@
from typing import Callable, Optional
from unittest import TestCase as _TestCase

from _pytest.logging import LogCaptureFixture

import jaclang
from jaclang.compiler.passes import Pass
from jaclang.utils.helpers import get_ast_nodes_as_snake_case as ast_snakes

import pytest


class TestCase(_TestCase):
"""Base test case for Jaseci."""

# Reference: https://stackoverflow.com/a/50375022
@pytest.fixture(autouse=True)
def inject_fixtures(self, caplog: LogCaptureFixture) -> None:
"""Store the logger capture records within the tests."""
self.caplog = caplog

def setUp(self) -> None:
"""Set up test case."""
return super().setUp()
Expand Down

0 comments on commit c7d715d

Please sign in to comment.