t-bug-catcher 0.6.13__tar.gz → 0.6.14__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {t_bug_catcher-0.6.13 → t_bug_catcher-0.6.14}/PKG-INFO +1 -1
- {t_bug_catcher-0.6.13 → t_bug_catcher-0.6.14}/setup.cfg +1 -1
- {t_bug_catcher-0.6.13 → t_bug_catcher-0.6.14}/setup.py +1 -1
- {t_bug_catcher-0.6.13 → t_bug_catcher-0.6.14}/t_bug_catcher/__init__.py +1 -1
- {t_bug_catcher-0.6.13 → t_bug_catcher-0.6.14}/t_bug_catcher/jira.py +39 -9
- {t_bug_catcher-0.6.13 → t_bug_catcher-0.6.14}/t_bug_catcher.egg-info/PKG-INFO +1 -1
- {t_bug_catcher-0.6.13 → t_bug_catcher-0.6.14}/t_bug_catcher.egg-info/SOURCES.txt +1 -0
- t_bug_catcher-0.6.14/tests/test_jira_grouping.py +112 -0
- {t_bug_catcher-0.6.13 → t_bug_catcher-0.6.14}/MANIFEST.in +0 -0
- {t_bug_catcher-0.6.13 → t_bug_catcher-0.6.14}/README.rst +0 -0
- {t_bug_catcher-0.6.13 → t_bug_catcher-0.6.14}/pyproject.toml +0 -0
- {t_bug_catcher-0.6.13 → t_bug_catcher-0.6.14}/requirements.txt +0 -0
- {t_bug_catcher-0.6.13 → t_bug_catcher-0.6.14}/t_bug_catcher/bug_catcher.py +0 -0
- {t_bug_catcher-0.6.13 → t_bug_catcher-0.6.14}/t_bug_catcher/bug_snag.py +0 -0
- {t_bug_catcher-0.6.13 → t_bug_catcher-0.6.14}/t_bug_catcher/config.py +0 -0
- {t_bug_catcher-0.6.13 → t_bug_catcher-0.6.14}/t_bug_catcher/exceptions.py +0 -0
- {t_bug_catcher-0.6.13 → t_bug_catcher-0.6.14}/t_bug_catcher/resources/whispers_config.yml +0 -0
- {t_bug_catcher-0.6.13 → t_bug_catcher-0.6.14}/t_bug_catcher/stack_saver.py +0 -0
- {t_bug_catcher-0.6.13 → t_bug_catcher-0.6.14}/t_bug_catcher/utils/__init__.py +0 -0
- {t_bug_catcher-0.6.13 → t_bug_catcher-0.6.14}/t_bug_catcher/utils/common.py +0 -0
- {t_bug_catcher-0.6.13 → t_bug_catcher-0.6.14}/t_bug_catcher/utils/logger.py +0 -0
- {t_bug_catcher-0.6.13 → t_bug_catcher-0.6.14}/t_bug_catcher/workitems.py +0 -0
- {t_bug_catcher-0.6.13 → t_bug_catcher-0.6.14}/t_bug_catcher.egg-info/dependency_links.txt +0 -0
- {t_bug_catcher-0.6.13 → t_bug_catcher-0.6.14}/t_bug_catcher.egg-info/not-zip-safe +0 -0
- {t_bug_catcher-0.6.13 → t_bug_catcher-0.6.14}/t_bug_catcher.egg-info/requires.txt +0 -0
- {t_bug_catcher-0.6.13 → t_bug_catcher-0.6.14}/t_bug_catcher.egg-info/top_level.txt +0 -0
- {t_bug_catcher-0.6.13 → t_bug_catcher-0.6.14}/tests/test_t_bug_catcher.py +0 -0
|
@@ -26,7 +26,7 @@ setup(
|
|
|
26
26
|
packages=find_packages(include=["t_bug_catcher", "t_bug_catcher.*"]),
|
|
27
27
|
test_suite="tests",
|
|
28
28
|
url="https://www.thoughtful.ai/",
|
|
29
|
-
version="0.6.
|
|
29
|
+
version="0.6.14",
|
|
30
30
|
zip_safe=False,
|
|
31
31
|
install_requires=install_requirements,
|
|
32
32
|
include_package_data=True,
|
|
@@ -164,6 +164,31 @@ class Jira:
|
|
|
164
164
|
"Content-Type": "application/json",
|
|
165
165
|
}
|
|
166
166
|
|
|
167
|
+
@staticmethod
|
|
168
|
+
def _adf_to_text(adf) -> str:
|
|
169
|
+
"""Extract plaintext from Atlassian Document Format (ADF) nodes.
|
|
170
|
+
|
|
171
|
+
Args:
|
|
172
|
+
adf: ADF structure (dict/list) or a plain string.
|
|
173
|
+
|
|
174
|
+
Returns:
|
|
175
|
+
str: Concatenated text content.
|
|
176
|
+
"""
|
|
177
|
+
if not adf:
|
|
178
|
+
return ""
|
|
179
|
+
if isinstance(adf, str):
|
|
180
|
+
return adf
|
|
181
|
+
if isinstance(adf, list):
|
|
182
|
+
return "".join(Jira._adf_to_text(item) for item in adf)
|
|
183
|
+
if isinstance(adf, dict):
|
|
184
|
+
# Prefer explicit text if present
|
|
185
|
+
text_value = adf.get("text")
|
|
186
|
+
if isinstance(text_value, str):
|
|
187
|
+
return text_value
|
|
188
|
+
# Otherwise, walk nested content
|
|
189
|
+
return Jira._adf_to_text(adf.get("content", []))
|
|
190
|
+
return ""
|
|
191
|
+
|
|
167
192
|
@retry_if_bad_request
|
|
168
193
|
def get_issues(self, project_key: Optional[str] = None) -> dict:
|
|
169
194
|
"""A function to get the issues using a Jira API.
|
|
@@ -1406,11 +1431,15 @@ class Jira:
|
|
|
1406
1431
|
all_issues = self.get_issues()["issues"]
|
|
1407
1432
|
|
|
1408
1433
|
if group_id:
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1434
|
+
search_prefix = f"Error string ID: {error_id}"
|
|
1435
|
+
existing_tickets = []
|
|
1436
|
+
for ticket in all_issues:
|
|
1437
|
+
description = ticket.get("fields", {}).get("description")
|
|
1438
|
+
if not description:
|
|
1439
|
+
continue
|
|
1440
|
+
description_text = self._adf_to_text(description)
|
|
1441
|
+
if search_prefix in description_text:
|
|
1442
|
+
existing_tickets.append(ticket)
|
|
1414
1443
|
summary = self.__create_summary(
|
|
1415
1444
|
type(exception),
|
|
1416
1445
|
exception,
|
|
@@ -1724,13 +1753,14 @@ class Jira:
|
|
|
1724
1753
|
Returns:
|
|
1725
1754
|
dict or None: The matching ticket if found, otherwise None.
|
|
1726
1755
|
"""
|
|
1756
|
+
search_token = f"Error string ID: {error_id}"
|
|
1727
1757
|
for ticket in all_tickets:
|
|
1728
|
-
description = ticket.get("fields", {}).get("description"
|
|
1758
|
+
description = ticket.get("fields", {}).get("description")
|
|
1729
1759
|
if not description:
|
|
1730
1760
|
continue
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1761
|
+
description_text = self._adf_to_text(description)
|
|
1762
|
+
if search_token in description_text:
|
|
1763
|
+
return ticket
|
|
1734
1764
|
|
|
1735
1765
|
else:
|
|
1736
1766
|
return None
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import unittest
|
|
2
|
+
from unittest.mock import MagicMock, patch
|
|
3
|
+
|
|
4
|
+
from t_bug_catcher.jira import Jira
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class TestJiraGrouping(unittest.TestCase):
|
|
8
|
+
"""Tests for Jira ADF parsing and ticket grouping logic."""
|
|
9
|
+
|
|
10
|
+
def setUp(self):
|
|
11
|
+
"""Set up the test environment."""
|
|
12
|
+
self.jira = Jira()
|
|
13
|
+
|
|
14
|
+
def test_adf_to_text_extracts_error_id(self):
|
|
15
|
+
"""Ensure ADF parser extracts plain text including the error id token."""
|
|
16
|
+
error_id = "abc123"
|
|
17
|
+
adf = {
|
|
18
|
+
"type": "doc",
|
|
19
|
+
"version": 1,
|
|
20
|
+
"content": [
|
|
21
|
+
{
|
|
22
|
+
"type": "paragraph",
|
|
23
|
+
"content": [
|
|
24
|
+
{"type": "text", "text": "Error string ID: "},
|
|
25
|
+
{"type": "text", "text": error_id, "marks": [{"type": "em"}]},
|
|
26
|
+
],
|
|
27
|
+
}
|
|
28
|
+
],
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
text = Jira._adf_to_text(adf)
|
|
32
|
+
self.assertIn(f"Error string ID: {error_id}", text)
|
|
33
|
+
|
|
34
|
+
def test_filter_tickets_matches_adf_description(self):
|
|
35
|
+
"""filter_tickets should find a ticket when the error id is embedded in ADF description."""
|
|
36
|
+
error_id = "abc123"
|
|
37
|
+
ticket = {
|
|
38
|
+
"id": "10001",
|
|
39
|
+
"key": "TEST-1",
|
|
40
|
+
"fields": {
|
|
41
|
+
"description": {
|
|
42
|
+
"type": "doc",
|
|
43
|
+
"version": 1,
|
|
44
|
+
"content": [
|
|
45
|
+
{
|
|
46
|
+
"type": "paragraph",
|
|
47
|
+
"content": [
|
|
48
|
+
{"type": "text", "text": f"Error string ID: {error_id}"},
|
|
49
|
+
],
|
|
50
|
+
}
|
|
51
|
+
],
|
|
52
|
+
}
|
|
53
|
+
},
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
matched = self.jira.filter_tickets(all_tickets=[ticket], error_id=error_id)
|
|
57
|
+
self.assertIsNotNone(matched)
|
|
58
|
+
self.assertEqual(matched["key"], "TEST-1")
|
|
59
|
+
|
|
60
|
+
@patch.object(Jira, "_Jira__update_existing_ticket")
|
|
61
|
+
@patch.object(Jira, "_Jira__create_new_ticket")
|
|
62
|
+
@patch.object(Jira, "get_issues")
|
|
63
|
+
@patch.object(Jira, "_Jira__generate_error_id")
|
|
64
|
+
def test_report_error_groups_updates_existing(self, mock_gen_id, mock_get_issues, mock_create, mock_update):
|
|
65
|
+
"""report_error should create once, then update existing when the same error is reported again."""
|
|
66
|
+
fixed_id = "deadbeef"
|
|
67
|
+
mock_gen_id.return_value = fixed_id
|
|
68
|
+
|
|
69
|
+
# First call: no issues present -> should create
|
|
70
|
+
# Second call: an issue present with matching error id in ADF -> should update
|
|
71
|
+
adf_issue = {
|
|
72
|
+
"id": "10002",
|
|
73
|
+
"key": "TEST-2",
|
|
74
|
+
"fields": {
|
|
75
|
+
"description": {
|
|
76
|
+
"type": "doc",
|
|
77
|
+
"version": 1,
|
|
78
|
+
"content": [
|
|
79
|
+
{
|
|
80
|
+
"type": "paragraph",
|
|
81
|
+
"content": [
|
|
82
|
+
{"type": "text", "text": f"Error string ID: {fixed_id}"},
|
|
83
|
+
],
|
|
84
|
+
}
|
|
85
|
+
],
|
|
86
|
+
}
|
|
87
|
+
},
|
|
88
|
+
}
|
|
89
|
+
mock_get_issues.side_effect = [
|
|
90
|
+
{"issues": []},
|
|
91
|
+
{"issues": [adf_issue]},
|
|
92
|
+
]
|
|
93
|
+
|
|
94
|
+
# Prevent any network side effects
|
|
95
|
+
mock_create.return_value = MagicMock(status_code=201, json=lambda: {"key": "TEST-NEW", "id": "10010"})
|
|
96
|
+
mock_update.return_value = None
|
|
97
|
+
|
|
98
|
+
# Raise and catch the same exception twice to keep traceback and ids deterministic
|
|
99
|
+
for call_idx in range(2):
|
|
100
|
+
try:
|
|
101
|
+
d = {"a": 1}
|
|
102
|
+
_ = d["missing"]
|
|
103
|
+
except Exception as ex:
|
|
104
|
+
self.jira.report_error(exception=ex)
|
|
105
|
+
|
|
106
|
+
# Should create once and then update once
|
|
107
|
+
self.assertEqual(mock_create.call_count, 1)
|
|
108
|
+
self.assertEqual(mock_update.call_count, 1)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
if __name__ == "__main__":
|
|
112
|
+
unittest.main()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|