Repository: microsoft/ptvsd

Issue:
Verify django and jinja2 exceptions context
See: https://github.com/Microsoft/ptvsd/pull/1179/files/90dc0b17a0bceadbc0dad958be04ad5f2fa81404#diff-a3b5595e743c389ebd7b61a7ab4c3c80R131


Discussion:

Resolving patch:
diff --git a/src/ptvsd/_vendored/pydevd/_pydevd_bundle/pydevd_frame.py b/src/ptvsd/_vendored/pydevd/_pydevd_bundle/pydevd_frame.py
--- a/src/ptvsd/_vendored/pydevd/_pydevd_bundle/pydevd_frame.py
+++ b/src/ptvsd/_vendored/pydevd/_pydevd_bundle/pydevd_frame.py
@@ -118,62 +118,64 @@ def should_stop_on_exception(self, frame, event, arg):
             if trace is not None and hasattr(trace, 'tb_next'):
                 # on jython trace is None on the first event and it may not have a tb_next.
 
-                exception_breakpoint = main_debugger.get_exception_breakpoint(
-                    exception, main_debugger.break_on_caught_exceptions)
+                should_stop = False
+                exception_breakpoint = None
+                try:
+                    if main_debugger.plugin is not None:
+                        result = main_debugger.plugin.exception_break(main_debugger, self, frame, self._args, arg)
+                        if result:
+                            should_stop, frame = result
+                except:
+                    traceback.print_exc()
 
-                if exception_breakpoint is not None:
-                    if exception_breakpoint.condition is not None:
-                        eval_result = main_debugger.handle_breakpoint_condition(info, exception_breakpoint, frame)
-                        if not eval_result:
-                            return False, frame
+                if not should_stop:
+                    # It was not handled by any plugin, lets check exception breakpoints.
+                    exception_breakpoint = main_debugger.get_exception_breakpoint(
+                        exception, main_debugger.break_on_caught_exceptions)
 
-                    if exception_breakpoint.ignore_libraries:
-                        if not main_debugger.is_exception_trace_in_project_scope(trace):
-                            pydev_log.debug("Ignore exception %s in library %s -- (%s)" % (exception, frame.f_code.co_filename, frame.f_code.co_name))
-                            return False, frame
-
-                    if ignore_exception_trace(trace):
-                        return False, frame
+                    if exception_breakpoint is not None:
+                        if exception_breakpoint.condition is not None:
+                            eval_result = main_debugger.handle_breakpoint_condition(info, exception_breakpoint, frame)
+                            if not eval_result:
+                                return False, frame
 
-                    was_just_raised = just_raised(trace)
-                    if was_just_raised:
+                        if exception_breakpoint.ignore_libraries:
+                            if not main_debugger.is_exception_trace_in_project_scope(trace):
+                                pydev_log.debug("Ignore exception %s in library %s -- (%s)" % (exception, frame.f_code.co_filename, frame.f_code.co_name))
+                                return False, frame
 
-                        if main_debugger.skip_on_exceptions_thrown_in_same_context:
-                            # Option: Don't break if an exception is caught in the same function from which it is thrown
+                        if ignore_exception_trace(trace):
                             return False, frame
 
-                    if exception_breakpoint.notify_on_first_raise_only:
-                        if main_debugger.skip_on_exceptions_thrown_in_same_context:
-                            # In this case we never stop if it was just raised, so, to know if it was the first we
-                            # need to check if we're in the 2nd method.
-                            if not was_just_raised and not just_raised(trace.tb_next):
-                                return False, frame  # I.e.: we stop only when we're at the caller of a method that throws an exception
+                        was_just_raised = just_raised(trace)
+                        if was_just_raised:
 
-                        else:
-                            if not was_just_raised:
-                                return False, frame  # I.e.: we stop only when it was just raised
+                            if main_debugger.skip_on_exceptions_thrown_in_same_context:
+                                # Option: Don't break if an exception is caught in the same function from which it is thrown
+                                return False, frame
 
-                    # If it got here we should stop.
-                    should_stop = True
-                    try:
-                        info.pydev_message = exception_breakpoint.qname
-                    except:
-                        info.pydev_message = exception_breakpoint.qname.encode('utf-8')
+                        if exception_breakpoint.notify_on_first_raise_only:
+                            if main_debugger.skip_on_exceptions_thrown_in_same_context:
+                                # In this case we never stop if it was just raised, so, to know if it was the first we
+                                # need to check if we're in the 2nd method.
+                                if not was_just_raised and not just_raised(trace.tb_next):
+                                    return False, frame  # I.e.: we stop only when we're at the caller of a method that throws an exception
 
-                    # Always add exception to frame (must remove later after we proceed).
-                    add_exception_to_frame(frame, (exception, value, trace))
+                            else:
+                                if not was_just_raised:
+                                    return False, frame  # I.e.: we stop only when it was just raised
 
-                else:
-                    # No regular exception breakpoint, let's see if some plugin handles it.
-                    try:
-                        if main_debugger.plugin is not None:
-                            result = main_debugger.plugin.exception_break(main_debugger, self, frame, self._args, arg)
-                            if result:
-                                should_stop, frame = result
-                    except:
-                        should_stop = False
+                        # If it got here we should stop.
+                        should_stop = True
+                        try:
+                            info.pydev_message = exception_breakpoint.qname
+                        except:
+                            info.pydev_message = exception_breakpoint.qname.encode('utf-8')
 
                 if should_stop:
+                    # Always add exception to frame (must remove later after we proceed).
+                    add_exception_to_frame(frame, (exception, value, trace))
+
                     if exception_breakpoint is not None and exception_breakpoint.expression is not None:
                         main_debugger.handle_breakpoint_expression(exception_breakpoint, info, frame)
 
diff --git a/src/ptvsd/_vendored/pydevd/pydevd_plugins/django_debug.py b/src/ptvsd/_vendored/pydevd/pydevd_plugins/django_debug.py
--- a/src/ptvsd/_vendored/pydevd/pydevd_plugins/django_debug.py
+++ b/src/ptvsd/_vendored/pydevd/pydevd_plugins/django_debug.py
@@ -7,6 +7,7 @@
 from _pydevd_bundle.pydevd_constants import STATE_SUSPEND, dict_iter_items, DJANGO_SUSPEND, IS_PY2
 from _pydevd_bundle.pydevd_frame_utils import add_exception_to_frame, FCode, just_raised, ignore_exception_trace
 from pydevd_file_utils import get_abs_path_real_path_and_base_from_file, normcase
+import os
 
 IS_DJANGO18 = False
 IS_DJANGO19 = False
@@ -84,7 +85,7 @@ def _inherits(cls, *names):
     return inherits_node
 
 
-def _is_django_render_call(frame):
+def _is_django_render_call(frame, debug=False):
     try:
         name = frame.f_code.co_name
         if name != 'render':
@@ -150,8 +151,6 @@ def _is_django_suspended(thread):
 
 
 def suspend_django(main_debugger, thread, frame, cmd=CMD_SET_BREAK):
-    frame = DjangoTemplateFrame(frame)
-
     if frame.f_lineno is None:
         return None
 
@@ -231,7 +230,7 @@ def _get_template_file_name(frame):
             if 'context' in frame.f_locals:
                 context = frame.f_locals['context']
                 if hasattr(context, '_has_included_template'):
-                    #  if there was included template we need to inspect the previous frames and find its name
+                    # if there was included template we need to inspect the previous frames and find its name
                     back = frame.f_back
                     while back is not None and frame.f_code.co_name in ('render', '_render'):
                         locals = back.f_locals
@@ -287,19 +286,19 @@ def _get_template_line(frame):
         return None
 
 
-class DjangoTemplateFrame:
+class DjangoTemplateFrame(object):
 
     def __init__(self, frame):
         file_name = _get_template_file_name(frame)
-        self.back_context = frame.f_locals['context']
+        self._back_context = frame.f_locals['context']
         self.f_code = FCode('Django Template', file_name)
         self.f_lineno = _get_template_line(frame)
         self.f_back = frame
         self.f_globals = {}
-        self.f_locals = self.collect_context(self.back_context)
+        self.f_locals = self._collect_context(self._back_context)
         self.f_trace = None
 
-    def collect_context(self, context):
+    def _collect_context(self, context):
         res = {}
         try:
             for d in context.dicts:
@@ -310,12 +309,23 @@ def collect_context(self, context):
         return res
 
     def _change_variable(self, name, value):
-        for d in self.back_context.dicts:
+        for d in self._back_context.dicts:
             for k, v in d.items():
                 if k == name:
                     d[k] = value
 
 
+class DjangoTemplateSyntaxErrorFrame(object):
+
+    def __init__(self, frame, filename, lineno, f_locals):
+        self.f_code = FCode('Django TemplateSyntaxError', filename)
+        self.f_lineno = lineno
+        self.f_back = frame
+        self.f_globals = {}
+        self.f_locals = f_locals
+        self.f_trace = None
+
+
 def change_variable(plugin, frame, attr, expression):
     if isinstance(frame, DjangoTemplateFrame):
         result = eval(expression, frame.f_globals, frame.f_locals)
@@ -324,12 +334,12 @@ def change_variable(plugin, frame, attr, expression):
     return False
 
 
-def _is_django_exception_break_context(frame):
+def _is_django_variable_does_not_exist_exception_break_context(frame):
     try:
         name = frame.f_code.co_name
     except:
         name = None
-    return name in ['_resolve_lookup', 'find_template']
+    return name in ('_resolve_lookup', 'find_template')
 
 #=======================================================================================================================
 # Django Step Commands
@@ -389,7 +399,7 @@ def stop(plugin, main_debugger, frame, event, args, stop_info, arg, step_cmd):
     main_debugger = args[0]
     thread = args[3]
     if 'django_stop' in stop_info and stop_info['django_stop']:
-        frame = suspend_django(main_debugger, thread, frame, step_cmd)
+        frame = suspend_django(main_debugger, thread, DjangoTemplateFrame(frame), step_cmd)
         if frame:
             main_debugger.do_wait_suspend(thread, frame, event, arg)
             return True
@@ -424,7 +434,7 @@ def get_breakpoint(plugin, main_debugger, pydb_frame, frame, event, args):
 
 def suspend(plugin, main_debugger, thread, frame, bp_type):
     if bp_type == 'django':
-        return suspend_django(main_debugger, thread, frame)
+        return suspend_django(main_debugger, thread, DjangoTemplateFrame(frame))
     return None
 
 
@@ -432,17 +442,48 @@ def exception_break(plugin, main_debugger, pydb_frame, frame, args, arg):
     main_debugger = args[0]
     thread = args[3]
     exception, value, trace = arg
-    if main_debugger.django_exception_break and exception is not None and \
-            exception.__name__ in ['VariableDoesNotExist', 'TemplateDoesNotExist', 'TemplateSyntaxError'] and \
-            just_raised(trace) and not ignore_exception_trace(trace) and _is_django_exception_break_context(frame):
-        render_frame = _find_django_render_frame(frame)
-        if render_frame:
-            suspend_frame = suspend_django(main_debugger, thread, render_frame, CMD_ADD_EXCEPTION_BREAK)
-            if suspend_frame:
-                add_exception_to_frame(suspend_frame, (exception, value, trace))
-                flag = True
-                thread.additional_info.pydev_message = 'VariableDoesNotExist'
-                suspend_frame.f_back = frame
-                frame = suspend_frame
-                return (flag, frame)
+
+    if main_debugger.django_exception_break and exception is not None:
+        if exception.__name__ in ['VariableDoesNotExist', 'TemplateDoesNotExist', 'TemplateSyntaxError'] and \
+                just_raised(trace) and not ignore_exception_trace(trace):
+
+            if exception.__name__ == 'TemplateSyntaxError':
+                # In this case we don't actually have a regular render frame with the context
+                # (we didn't really get to that point).
+                token = getattr(value, 'token', None)
+                lineno = getattr(token, 'lineno', None)
+                filename = None
+                if lineno is not None:
+                    get_template_frame = frame
+                    while get_template_frame.f_code.co_name != 'get_template':
+                        get_template_frame = get_template_frame.f_back
+
+                    origin = None
+                    if get_template_frame is not None:
+                        origin = get_template_frame.f_locals.get('origin')
+
+                    if hasattr(origin, 'name') and origin.name is not None:
+                        filename = normcase(origin.name)
+
+                if filename is not None and lineno is not None:
+                    syntax_error_frame = DjangoTemplateSyntaxErrorFrame(
+                        frame, filename, lineno, {'token': token, 'exception': exception})
+
+                    suspend_frame = suspend_django(
+                        main_debugger, thread, syntax_error_frame, CMD_ADD_EXCEPTION_BREAK)
+                    return True, suspend_frame
+
+            elif exception.__name__ == 'VariableDoesNotExist':
+                if _is_django_variable_does_not_exist_exception_break_context(frame):
+                    render_frame = _find_django_render_frame(frame)
+                    if render_frame:
+                        suspend_frame = suspend_django(
+                            main_debugger, thread, DjangoTemplateFrame(render_frame), CMD_ADD_EXCEPTION_BREAK)
+                        if suspend_frame:
+                            add_exception_to_frame(suspend_frame, (exception, value, trace))
+                            thread.additional_info.pydev_message = 'VariableDoesNotExist'
+                            suspend_frame.f_back = frame
+                            frame = suspend_frame
+                            return True, frame
+
     return None


Test patch:
diff --git a/src/ptvsd/_vendored/pydevd/tests_python/debug_constants.py b/src/ptvsd/_vendored/pydevd/tests_python/debug_constants.py
--- a/src/ptvsd/_vendored/pydevd/tests_python/debug_constants.py
+++ b/src/ptvsd/_vendored/pydevd/tests_python/debug_constants.py
@@ -19,3 +19,18 @@
 IS_PY36 = False
 if sys.version_info[0] == 3 and sys.version_info[1] == 6:
     IS_PY36 = True
+
+TEST_DJANGO = False
+TEST_FLASK = False
+
+try:
+    import django
+    TEST_DJANGO = True
+except:
+    pass
+
+try:
+    import flask
+    TEST_FLASK = True
+except:
+    pass
diff --git a/src/ptvsd/_vendored/pydevd/tests_python/debugger_fixtures.py b/src/ptvsd/_vendored/pydevd/tests_python/debugger_fixtures.py
--- a/src/ptvsd/_vendored/pydevd/tests_python/debugger_fixtures.py
+++ b/src/ptvsd/_vendored/pydevd/tests_python/debugger_fixtures.py
@@ -173,6 +173,7 @@ def get_command_line_args(self):
             debugger_unittest._get_debugger_test_file(os.path.join(self.DJANGO_FOLDER, 'manage.py')),
             'runserver',
             '--noreload',
+            '--nothreading',
             str(free_port),
         ]
 
diff --git a/src/ptvsd/_vendored/pydevd/tests_python/my_django_proj_21/my_app/templates/my_app/template_error.html b/src/ptvsd/_vendored/pydevd/tests_python/my_django_proj_21/my_app/templates/my_app/template_error.html
new file mode 100644
--- /dev/null
+++ b/src/ptvsd/_vendored/pydevd/tests_python/my_django_proj_21/my_app/templates/my_app/template_error.html
@@ -0,0 +1,13 @@
+{% if entries %}
+    <ul>
+    {% for entry in entries %}
+        <li>
+            {{ entry.key }} 
+            : 
+            {{ entry.invalid_attribute }}
+        </li>
+    {% endfor %}
+    </ul>
+{% else %}
+    <p>No entries are available.</p>
+{% endif %}
\ No newline at end of file
diff --git a/src/ptvsd/_vendored/pydevd/tests_python/my_django_proj_21/my_app/templates/my_app/template_error2.html b/src/ptvsd/_vendored/pydevd/tests_python/my_django_proj_21/my_app/templates/my_app/template_error2.html
new file mode 100644
--- /dev/null
+++ b/src/ptvsd/_vendored/pydevd/tests_python/my_django_proj_21/my_app/templates/my_app/template_error2.html
@@ -0,0 +1,6 @@
+<!DOCTYPE html>
+<html>
+    <body>
+        {% doesnotexist %}
+    </body>
+</html>
\ No newline at end of file
diff --git a/src/ptvsd/_vendored/pydevd/tests_python/my_django_proj_21/my_app/urls.py b/src/ptvsd/_vendored/pydevd/tests_python/my_django_proj_21/my_app/urls.py
--- a/src/ptvsd/_vendored/pydevd/tests_python/my_django_proj_21/my_app/urls.py
+++ b/src/ptvsd/_vendored/pydevd/tests_python/my_django_proj_21/my_app/urls.py
@@ -5,4 +5,6 @@
 urlpatterns = [
     url(r'^$', views.index, name='index'),
     url(r'^name$', views.get_name, name='name'),
-]
\ No newline at end of file
+    url(r'^template_error2$', views.template_error2, name='template_error2'),
+    url(r'^template_error$', views.template_error, name='template_error'),
+]
diff --git a/src/ptvsd/_vendored/pydevd/tests_python/my_django_proj_21/my_app/views.py b/src/ptvsd/_vendored/pydevd/tests_python/my_django_proj_21/my_app/views.py
--- a/src/ptvsd/_vendored/pydevd/tests_python/my_django_proj_21/my_app/views.py
+++ b/src/ptvsd/_vendored/pydevd/tests_python/my_django_proj_21/my_app/views.py
@@ -48,3 +48,21 @@ def get_name(request):
         form = NameForm(data={'your_name': 'unknown name'})
 
     return render(request, 'my_app/name.html', {'form': form})
+
+
+def template_error(request):
+    import faulthandler
+    faulthandler.enable()
+    context = {
+        'entries': [Entry('v1', 'v1'), Entry('v2', 'v2')]
+    }
+    ret = render(request, 'my_app/template_error.html', context)
+    return ret
+
+
+def template_error2(request):
+    import faulthandler
+    faulthandler.enable()
+    context = {}
+    ret = render(request, 'my_app/template_error2.html', context)
+    return ret
diff --git a/src/ptvsd/_vendored/pydevd/tests_python/test_debugger.py b/src/ptvsd/_vendored/pydevd/tests_python/test_debugger.py
--- a/src/ptvsd/_vendored/pydevd/tests_python/test_debugger.py
+++ b/src/ptvsd/_vendored/pydevd/tests_python/test_debugger.py
@@ -26,7 +26,7 @@
 except ImportError:
     from urllib.parse import unquote
 
-from tests_python.debug_constants import *
+from tests_python.debug_constants import *  # noqa
 
 pytest_plugins = [
     str('tests_python.debugger_fixtures'),
@@ -37,21 +37,6 @@
 except:
     xrange = range
 
-TEST_DJANGO = False
-TEST_FLASK = False
-
-try:
-    import django
-    TEST_DJANGO = True
-except:
-    pass
-
-try:
-    import flask
-    TEST_FLASK = True
-except:
-    pass
-
 if IS_PY2:
     builtin_qualifier = "__builtin__"
 else:
@@ -968,6 +953,86 @@ def test_case_django_b(case_setup_django):
         writer.finished_ok = True
 
 
+@pytest.mark.skipif(not TEST_DJANGO, reason='No django available')
+def test_case_django_no_attribute_exception_breakpoint(case_setup_django):
+    django_version = [int(x) for x in django.get_version().split('.')][:2]
+
+    if django_version < [2, 1]:
+        pytest.skip('Template exceptions only supporting Django 2.1 onwards.')
+
+    with case_setup_django.test_file(EXPECTED_RETURNCODE='any') as writer:
+        writer.write_add_exception_breakpoint_django()
+        writer.write_make_initial_run()
+
+        t = writer.create_request_thread('my_app/template_error')
+        time.sleep(5)  # Give django some time to get to startup before requesting the page
+        t.start()
+
+        hit = writer.wait_for_breakpoint_hit(REASON_CAUGHT_EXCEPTION, line=7, file='template_error.html')
+
+        writer.write_get_frame(hit.thread_id, hit.frame_id)
+        writer.wait_for_var('<var name="entry" type="Entry" qualifier="my_app.views" value="Entry: v1:v1" isContainer="True"')
+
+        writer.write_run_thread(hit.thread_id)
+        writer.finished_ok = True
+
+
+@pytest.mark.skipif(not TEST_DJANGO, reason='No django available')
+def test_case_django_no_attribute_exception_breakpoint_and_regular_exceptions(case_setup_django):
+    django_version = [int(x) for x in django.get_version().split('.')][:2]
+
+    if django_version < [2, 1]:
+        pytest.skip('Template exceptions only supporting Django 2.1 onwards.')
+
+    with case_setup_django.test_file(EXPECTED_RETURNCODE='any') as writer:
+        writer.write_add_exception_breakpoint_django()
+
+        # The django plugin has priority over the regular exception breakpoint.
+        writer.write_add_exception_breakpoint_with_policy(
+            'django.template.base.VariableDoesNotExist',
+            notify_on_handled_exceptions=2,  # 2 means notify only on first raise.
+            notify_on_unhandled_exceptions=0,
+            ignore_libraries=0
+        )
+        writer.write_make_initial_run()
+
+        t = writer.create_request_thread('my_app/template_error')
+        time.sleep(5)  # Give django some time to get to startup before requesting the page
+        t.start()
+
+        hit = writer.wait_for_breakpoint_hit(REASON_CAUGHT_EXCEPTION, line=7, file='template_error.html')
+
+        writer.write_get_frame(hit.thread_id, hit.frame_id)
+        writer.wait_for_var('<var name="entry" type="Entry" qualifier="my_app.views" value="Entry: v1:v1" isContainer="True"')
+
+        writer.write_run_thread(hit.thread_id)
+        writer.finished_ok = True
+
+
+@pytest.mark.skipif(not TEST_DJANGO, reason='No django available')
+def test_case_django_invalid_template_exception_breakpoint(case_setup_django):
+    django_version = [int(x) for x in django.get_version().split('.')][:2]
+
+    if django_version < [2, 1]:
+        pytest.skip('Template exceptions only supporting Django 2.1 onwards.')
+
+    with case_setup_django.test_file(EXPECTED_RETURNCODE='any') as writer:
+        writer.write_add_exception_breakpoint_django()
+        writer.write_make_initial_run()
+
+        t = writer.create_request_thread('my_app/template_error2')
+        time.sleep(5)  # Give django some time to get to startup before requesting the page
+        t.start()
+
+        hit = writer.wait_for_breakpoint_hit(REASON_CAUGHT_EXCEPTION, line=4, file='template_error2.html')
+
+        writer.write_get_frame(hit.thread_id, hit.frame_id)
+        writer.wait_for_var('<var name="token" type="Token" qualifier="django.template.base" value="Token:')
+
+        writer.write_run_thread(hit.thread_id)
+        writer.finished_ok = True
+
+
 @pytest.mark.skipif(not TEST_CYTHON, reason='No cython available')
 def test_cython(case_setup):
     from _pydevd_bundle import pydevd_cython
diff --git a/src/ptvsd/_vendored/pydevd/tests_python/test_debugger_json.py b/src/ptvsd/_vendored/pydevd/tests_python/test_debugger_json.py
--- a/src/ptvsd/_vendored/pydevd/tests_python/test_debugger_json.py
+++ b/src/ptvsd/_vendored/pydevd/tests_python/test_debugger_json.py
@@ -4,13 +4,13 @@
 from _pydevd_bundle._debug_adapter.pydevd_base_schema import from_json
 from _pydevd_bundle._debug_adapter.pydevd_schema import ThreadEvent, ModuleEvent
 from tests_python import debugger_unittest
-from tests_python.debugger_unittest import IS_JYTHON, REASON_STEP_INTO, REASON_STEP_OVER
+from tests_python.debugger_unittest import IS_JYTHON, REASON_STEP_INTO, REASON_STEP_OVER, \
+    REASON_CAUGHT_EXCEPTION
 import json
 from collections import namedtuple
-from _pydevd_bundle.pydevd_constants import int_types, IS_WINDOWS
-from tests_python.debug_constants import IS_PY2
-from os.path import os
-from tests_python.debugger_unittest import CMD_LOAD_SOURCE
+from _pydevd_bundle.pydevd_constants import int_types
+from tests_python.debug_constants import *  # noqa
+import time
 
 pytest_plugins = [
     str('tests_python.debugger_fixtures'),
@@ -799,6 +799,50 @@ def get_environ(writer):
         writer.finished_ok = True
 
 
+@pytest.mark.skipif(not TEST_DJANGO, reason='No django available')
+def test_case_django_no_attribute_exception_breakpoint(case_setup_django):
+    django_version = [int(x) for x in django.get_version().split('.')][:2]
+
+    if django_version < [2, 1]:
+        pytest.skip('Template exceptions only supporting Django 2.1 onwards.')
+
+    with case_setup_django.test_file(EXPECTED_RETURNCODE='any') as writer:
+        json_facade = JsonFacade(writer)
+        writer.write_set_protocol('http_json')
+
+        writer.write_add_exception_breakpoint_django()
+        writer.write_make_initial_run()
+
+        t = writer.create_request_thread('my_app/template_error')
+        time.sleep(5)  # Give django some time to get to startup before requesting the page
+        t.start()
+
+        hit = writer.wait_for_breakpoint_hit(REASON_CAUGHT_EXCEPTION, line=7, file='template_error.html')
+
+        stack_trace_request = json_facade.write_request(
+            pydevd_schema.StackTraceRequest(pydevd_schema.StackTraceArguments(
+                threadId=hit.thread_id,
+                format={'module': True, 'line': True}
+        )))
+        stack_trace_response = json_facade.wait_for_response(stack_trace_request)
+        stack_trace_response_body = stack_trace_response.body
+        stack_frame = next(iter(stack_trace_response_body.stackFrames))
+        assert stack_frame['source']['path'].endswith('template_error.html')
+
+        json_hit = json_facade.get_stack_as_json_hit(hit.thread_id)
+        variables_response = json_facade.get_variables_response(json_hit.frameId)
+        entries = [x for x in variables_response.to_dict()['body']['variables'] if x['name'] == 'entry']
+        assert len(entries) == 1
+        variables_response = json_facade.get_variables_response(entries[0]['variablesReference'])
+        assert variables_response.to_dict()['body']['variables'] == [
+            {'name': 'key', 'value': "'v1'", 'type': 'str', 'evaluateName': 'entry.key', 'presentationHint': {'attributes': ['rawString']}},
+            {'name': 'val', 'value': "'v1'", 'type': 'str', 'evaluateName': 'entry.val', 'presentationHint': {'attributes': ['rawString']}}
+        ]
+
+        writer.write_run_thread(hit.thread_id)
+        writer.finished_ok = True
+
+
 if __name__ == '__main__':
     pytest.main(['-k', 'test_case_skipping_filters', '-s'])