Browse Source

Merge branch 'main' into spot-check

Dan Helfman 1 năm trước cách đây
mục cha
commit
89ce060dbd
3 tập tin đã thay đổi với 30 bổ sung7 xóa
  1. 1 0
      NEWS
  2. 16 1
      borgmatic/actions/json.py
  3. 13 6
      tests/unit/actions/test_json.py

+ 1 - 0
NEWS

@@ -3,6 +3,7 @@
    configured monitoring hooks.
  * #843: Add documentation link to Loki dashboard for borgmatic:
    https://torsion.org/borgmatic/docs/how-to/monitor-your-backups/#loki-hook
+ * #847: Fix "--json" error when Borg includes non-JSON warnings in JSON output.
  * Fix handling of the NO_COLOR environment variable to ignore an empty value.
  * Add documentation about backing up containerized databases by configuring borgmatic to exec into
    a container to run a dump command:

+ 16 - 1
borgmatic/actions/json.py

@@ -1,12 +1,27 @@
+import logging
 import json
 
 
+logger = logging.getLogger(__name__)
+
+
 def parse_json(borg_json_output, label):
     '''
     Given a Borg JSON output string, parse it as JSON into a dict. Inject the given borgmatic
     repository label into it and return the dict.
+
+    Raise JSONDecodeError if the JSON output cannot be parsed.
     '''
-    json_data = json.loads(borg_json_output)
+    lines = borg_json_output.splitlines()
+    start_line_index = 0
+
+    # Scan forward to find the first line starting with "{" and assume that's where the JSON starts.
+    for line_index, line in enumerate(lines):
+        if line.startswith('{'):
+            start_line_index = line_index
+            break
+
+    json_data = json.loads('\n'.join(lines[start_line_index:]))
 
     if 'repository' not in json_data:
         return json_data

+ 13 - 6
tests/unit/actions/test_json.py

@@ -1,25 +1,32 @@
+import pytest
 from flexmock import flexmock
 
 from borgmatic.actions import json as module
 
 
 def test_parse_json_loads_json_from_string():
-    flexmock(module.json).should_receive('loads').and_return({'repository': {'id': 'foo'}})
-
     assert module.parse_json('{"repository": {"id": "foo"}}', label=None) == {
         'repository': {'id': 'foo', 'label': ''}
     }
 
 
-def test_parse_json_injects_label_into_parsed_data():
-    flexmock(module.json).should_receive('loads').and_return({'repository': {'id': 'foo'}})
+def test_parse_json_skips_non_json_warnings_and_loads_subsequent_json():
+    assert module.parse_json(
+        '/non/existent/path: stat: [Errno 2] No such file or directory: /non/existent/path\n{"repository":\n{"id": "foo"}}',
+        label=None,
+    ) == {'repository': {'id': 'foo', 'label': ''}}
+
+
+def test_parse_json_skips_with_invalid_json_raises():
+    with pytest.raises(module.json.JSONDecodeError):
+        module.parse_json('this is not valid JSON }', label=None)
 
+
+def test_parse_json_injects_label_into_parsed_data():
     assert module.parse_json('{"repository": {"id": "foo"}}', label='bar') == {
         'repository': {'id': 'foo', 'label': 'bar'}
     }
 
 
 def test_parse_json_injects_nothing_when_repository_missing():
-    flexmock(module.json).should_receive('loads').and_return({'stuff': {'id': 'foo'}})
-
     assert module.parse_json('{"stuff": {"id": "foo"}}', label='bar') == {'stuff': {'id': 'foo'}}