Skip to content

Commit b2f543a

Browse files
authored
Try to give a more informative error as this is user facing (bugfix) (#2353)
* Try to give a more informative error as this is user facing The previous error gave basically no reason why the json was invalid. This improves the situation adding exactly what the json decoder thinks is invalid with a primitive ^ hightlight * Refactor into independent function and tests * Also test binary docs * Bytes are not supported on 3.5 as straight input bytes end up in the doc only when parsing a file and I cant be bothered to test that
1 parent 6c0e0d5 commit b2f543a

2 files changed

Lines changed: 75 additions & 1 deletion

File tree

checkbox-ng/plainbox/impl/exporter/jinja2.py

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,6 @@
5252
from plainbox.impl.unit.exporter import ExporterError
5353
from plainbox.impl.config import CheckboxINIParser
5454

55-
5655
#: Name-space prefix for Canonical Certification
5756
CERTIFICATION_NS = "com.canonical.certification::"
5857

@@ -83,6 +82,25 @@ def highlight_keys(text):
8382
return re.sub(r"(\w+:\s)", r"<b>\1</b>", text)
8483

8584

85+
def pretty_json_decode_error(
86+
error: json.decoder.JSONDecodeError, lines_around=3
87+
):
88+
lineno = error.lineno
89+
colno = error.colno
90+
lines = error.doc.splitlines()
91+
min_line = max(lineno - lines_around, 0)
92+
max_line = min(lineno + lines_around, len(lines) - 1)
93+
center = min(lineno + 1, len(lines))
94+
error_repr = lines[min_line:center]
95+
error_repr.append(" " * colno + "^^^ " + str(error))
96+
error_repr += lines[center:max_line]
97+
if isinstance(error_repr[0], str):
98+
return "\n".join(error_repr)
99+
else:
100+
# defer decoding here to avoid decoding the whole document
101+
return b"\n".join(error_repr).decode("utf8")
102+
103+
86104
class Jinja2SessionStateExporter(SessionStateExporterBase):
87105
"""Session state exporter that renders output using jinja2 template."""
88106

@@ -279,5 +297,7 @@ def validate_json(self, stream):
279297
s = raw.decode("utf-8") if type(raw) == bytes else raw
280298
json.loads(s)
281299
return []
300+
except json.decoder.JSONDecodeError as exc:
301+
return pretty_json_decode_error(exc)
282302
except Exception as exc:
283303
return [str(exc)]

checkbox-ng/plainbox/impl/exporter/test_jinja2.py

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,9 +27,11 @@
2727
from tempfile import TemporaryDirectory
2828
from textwrap import dedent
2929
from unittest import TestCase
30+
import json
3031
import os
3132

3233
from plainbox.impl.exporter.jinja2 import Jinja2SessionStateExporter
34+
from plainbox.impl.exporter.jinja2 import pretty_json_decode_error
3335
from plainbox.impl.result import MemoryJobResult
3436
from plainbox.impl.session.state import SessionMetaData
3537
from plainbox.impl.unit.exporter import ExporterError
@@ -165,3 +167,55 @@ def test_validation_json_throws(self):
165167
exporter.dump_from_session_manager(
166168
self.manager_single_job, stream
167169
)
170+
171+
172+
class PrettyJsonDecodeErrorTests(TestCase):
173+
174+
def _get_error(self, bad_json):
175+
try:
176+
json.loads(bad_json)
177+
except json.decoder.JSONDecodeError as e:
178+
return e
179+
self.fail("Expected JSONDecodeError was not raised")
180+
181+
def _check(self, bad_json):
182+
error = self._get_error(bad_json)
183+
result = pretty_json_decode_error(error)
184+
self.assertIsInstance(result, str)
185+
# The original error message should always be present
186+
self.assertIn(str(error), result)
187+
return result
188+
189+
def test_error_at_start(self):
190+
result = self._check("not json at all")
191+
self.assertIn("not json", result)
192+
193+
def test_trailing_comma_in_object(self):
194+
self._check('{"key": "value",}')
195+
196+
def test_trailing_comma_in_array(self):
197+
self._check("[1, 2, 3,]")
198+
199+
def test_missing_colon(self):
200+
self._check('{"key" "value"}')
201+
202+
def test_single_quotes(self):
203+
self._check("{'key': 'value'}")
204+
205+
def test_empty_string(self):
206+
self._check("")
207+
208+
def test_truncated_object(self):
209+
self._check('{"key": "value"')
210+
211+
def test_error_deep_in_long_document(self):
212+
bad_json = '{"a": 1, ' * 100 + "BROKEN}"
213+
self._check(bad_json)
214+
215+
def test_multiline_json(self):
216+
self._check('{\n "a": 1,\n "b": ,\n "c": 3\n}')
217+
218+
def test_result_contains_error_neighbourhood(self):
219+
bad_json = '{"good": 1, "bad": tralse, "other": 2}'
220+
result = self._check(bad_json)
221+
self.assertIn("tralse", result)

0 commit comments

Comments
 (0)