Skip to content

Commit 3de95ac

Browse files
Helen KoikeJeny Sadadia
andcommitted
kcidb: add kcidb_match tool
Add `tools` directory and add `kcidb_match.py` script there. Co-authored-by: Jeny Sadadia <jeny.sadadia@collabora.com> Signed-off-by: Jeny Sadadia <jeny.sadadia@collabora.com>
1 parent 811a4ba commit 3de95ac

2 files changed

Lines changed: 276 additions & 0 deletions

File tree

kcidb/tools/__init__.py

Whitespace-only changes.

kcidb/tools/kcidb_match.py

Lines changed: 276 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,276 @@
1+
#!/usr/bin/env python3
2+
3+
"""KCIDB auto-matching tool"""
4+
5+
6+
import json
7+
import sys
8+
import sqlite3
9+
import hashlib
10+
import logging
11+
import argparse
12+
from .pattern_validator import match_fields, validate_pattern_object
13+
14+
15+
# Constants
16+
DB_NAME = 'patterns.db'
17+
ORIGIN = 'maestro'
18+
KCIDB_IO_VERSION = {
19+
"major": 4,
20+
"minor": 3
21+
}
22+
23+
# Configure logging
24+
logging.basicConfig(level=logging.INFO)
25+
logger = logging.getLogger(__name__)
26+
27+
28+
class PatternDatabase:
29+
"""Class to handle DB table 'patterns'"""
30+
def __init__(self, db_name=DB_NAME):
31+
self.db_name = db_name
32+
self.setup_database()
33+
34+
def setup_database(self):
35+
"""Connect to DB and create 'patterns' table if doesn't exist"""
36+
with sqlite3.connect(self.db_name) as conn:
37+
cursor = conn.cursor()
38+
cursor.execute('''
39+
CREATE TABLE IF NOT EXISTS patterns (
40+
issue_id TEXT UNIQUE,
41+
issue_version INTEGER,
42+
pattern_object JSON
43+
)
44+
''')
45+
conn.commit()
46+
47+
def add_pattern(self, issue_id, issue_version, pattern_object):
48+
"""Add pattern object to DB"""
49+
with sqlite3.connect(self.db_name) as conn:
50+
cursor = conn.cursor()
51+
cursor.execute('''
52+
INSERT INTO patterns (issue_id, issue_version, pattern_object)
53+
VALUES (?, ?, json(?))
54+
ON CONFLICT(issue_id) DO UPDATE SET
55+
issue_version=excluded.issue_version,
56+
pattern_object=excluded.pattern_object
57+
''', (issue_id, issue_version, json.dumps(pattern_object)))
58+
conn.commit()
59+
60+
def remove_pattern(self, issue_id):
61+
"""Remove pattern object from DB"""
62+
with sqlite3.connect(self.db_name) as conn:
63+
cursor = conn.cursor()
64+
cursor.execute('DELETE FROM patterns WHERE issue_id = ?',
65+
(issue_id,))
66+
conn.commit()
67+
68+
def get_all_patterns(self):
69+
"""Retrieve all patterns objects from DB"""
70+
with sqlite3.connect(self.db_name) as conn:
71+
cursor = conn.cursor()
72+
cursor.execute('SELECT issue_id, issue_version, pattern_object '
73+
'FROM patterns')
74+
while True:
75+
row = cursor.fetchone()
76+
if row is None:
77+
break
78+
yield row
79+
80+
def update_patterns(self, issue):
81+
"""Update patterns for existing issue"""
82+
if not issue.misc:
83+
return
84+
85+
if not issue.misc.get("pattern_object"):
86+
self.remove_pattern(issue.id)
87+
return
88+
89+
pattern_object = issue.misc.get("pattern_object")
90+
if not validate_pattern_object(pattern_object):
91+
logger.error("Pattern object validation failed for issue id: %s",
92+
issue.id)
93+
return
94+
self.add_pattern(issue.id, issue.version, pattern_object)
95+
96+
97+
class IncidentGenerator:
98+
"""Class to generate incidents"""
99+
def __init__(self, db_name=DB_NAME):
100+
self.db = PatternDatabase(db_name)
101+
102+
def create_incident(self, kcidb_io_object, issue_id, issue_version):
103+
"""Create and return an incident object"""
104+
if tests := kcidb_io_object.get('tests'):
105+
type_id_key = "test_id"
106+
type_id_value = tests[0]['id']
107+
elif builds := kcidb_io_object.get('builds'):
108+
type_id_key = "build_id"
109+
type_id_value = builds[0]['id']
110+
else:
111+
raise ValueError("The KCIDB IO object must contain at least "
112+
"one non-empty test or build")
113+
114+
unique_string = f"{issue_id}{issue_version}{type_id_value}"
115+
incident_id = f"{ORIGIN}:" \
116+
f"{hashlib.sha256(unique_string.encode()).hexdigest()}"
117+
118+
return {
119+
'id': incident_id,
120+
'origin': ORIGIN,
121+
'issue_id': issue_id,
122+
'issue_version': issue_version,
123+
'present': True,
124+
type_id_key: type_id_value,
125+
}
126+
127+
def generate_incident_on_match(self, kcidb_io_object, issue_id,
128+
issue_version, issue_pattern_object):
129+
"""Generate incident if issue pattern is found in a build/test
130+
object"""
131+
incident = {}
132+
133+
if match_fields(issue_pattern_object, kcidb_io_object):
134+
incident = self.create_incident(kcidb_io_object, issue_id,
135+
issue_version)
136+
137+
return incident
138+
139+
def generate_incidents_from_db(self, kcidb_io_object):
140+
"""
141+
Generate incidents by trying to match the kcidb_io_object
142+
against the patterns saved in the database
143+
"""
144+
incidents = []
145+
146+
for row in self.db.get_all_patterns():
147+
issue_id, issue_version, pattern_object_json = row
148+
pattern_object = json.loads(pattern_object_json)
149+
incident = self.generate_incident_on_match(
150+
kcidb_io_object, issue_id, issue_version, pattern_object)
151+
if incident:
152+
incidents.append(incident)
153+
154+
return {
155+
"version": KCIDB_IO_VERSION,
156+
"incidents": incidents
157+
}
158+
159+
def generate_incidents_from_test(self, test):
160+
"""Generate incident from test object"""
161+
kcidb_io_object = {"tests": [test._data],
162+
"builds": [test.build._data],
163+
"checkouts": [test.build.checkout._data]}
164+
return self.generate_incidents_from_db(kcidb_io_object)
165+
166+
def generate_incidents_from_build(self, build):
167+
"""Generate incident from build object"""
168+
kcidb_io_object = {"builds": [build._data],
169+
"checkouts": [build.checkout._data]}
170+
return self.generate_incidents_from_db(kcidb_io_object)
171+
172+
173+
def parse_arguments():
174+
"""Parse command-line arguments"""
175+
class CustomHelpFormatter(argparse.RawTextHelpFormatter):
176+
"""Help string formatter for command-line tools"""
177+
178+
parser = argparse.ArgumentParser(
179+
description='KCIDB Match Tool',
180+
formatter_class=CustomHelpFormatter,
181+
epilog='''\
182+
Usage examples:
183+
184+
export -x DB_OPTS="postgresql:host=127.0.0.1 port=5432 sslmode=disable
185+
dbname=playground_kcidb user=helen.koike@collabora.com"
186+
187+
# Update patterns
188+
kcidb-query -i "kernelci_api:70d17807303641a9d6d2a8aeb1aee829221cefcf"
189+
-d "$DB_OPTS" | ./kcidb-match.py --update-patterns
190+
191+
# Generate incidents
192+
kcidb-query -t "maestro:6690dbfc7488a1b744200e82" -d "$DB_OPTS"
193+
--parents | ./kcidb-match.py --generate-incidents
194+
195+
# Check test ID
196+
cat issue.json | ./kcidb-match.py --check_test_id
197+
"maestro:6690dbfc7488a1b744200e82" -d "$DB_OPTS"
198+
199+
# Check build ID
200+
cat issue.json | ./kcidb-match.py --check_build_id
201+
"maestro:6690dbfc7488a1b744200e82" -d "$DB_OPTS"
202+
'''
203+
)
204+
205+
parser.add_argument('--update-patterns', action='store_true',
206+
help='Update patterns from issues. Other '
207+
'arguments are ignored when used. Expects '
208+
'KCIDB-IO object with issues via stdin.')
209+
210+
parser.add_argument('--generate-incidents', action='store_true',
211+
help='Generate incidents for matched issues. '
212+
'Expects KCIDB-IO object with build and/or '
213+
'test via stdin.')
214+
215+
parser.add_argument('--ignore-db', action='store_true',
216+
help='Ignore the database and generate incidents '
217+
'based on the issues field in the KCIDB-IO '
218+
'object via stdin.')
219+
220+
parser.add_argument('--check_test_id', type=str,
221+
help='Test ID to check. Requires --db_conn. '
222+
'Implies --ignore-db. Expects KCIDB-IO '
223+
'object with issues via stdin.')
224+
225+
parser.add_argument('--check_build_id', type=str,
226+
help='Build ID to check. Requires --db_conn. '
227+
'Implies --ignore-db. '
228+
'Expects KCIDB-IO object with issues via stdin.')
229+
230+
parser.add_argument('-d', '--db_conn', type=str,
231+
help='Database connection string for kcidb-query.'
232+
'Required with --check_test_id or '
233+
'--check_build_id.')
234+
235+
args = parser.parse_args()
236+
237+
if args.check_test_id and args.check_build_id:
238+
parser.error("Cannot use both --check_test_id and --check_build_id")
239+
240+
if (args.check_test_id or args.check_build_id) and not args.db_conn:
241+
parser.error("--db_conn is required when using --check_test_id or "
242+
"--check_build_id")
243+
244+
if args.check_test_id or args.check_build_id:
245+
args.ignore_db = True
246+
247+
return args
248+
249+
250+
def main():
251+
"""Main function"""
252+
args = parse_arguments()
253+
254+
if args.update_patterns:
255+
issue_objects = json.load(sys.stdin)
256+
IncidentGenerator().db.update_patterns(issue_objects)
257+
return
258+
259+
kcidb_io_object = json.load(sys.stdin)
260+
261+
incident_generator = IncidentGenerator()
262+
263+
results = incident_generator.generate_incidents_from_db(
264+
kcidb_io_object)
265+
266+
if args.generate_incidents:
267+
print(json.dumps(results, indent=2))
268+
return
269+
270+
for incident in results['incidents']:
271+
print("Matched issue ID:", incident['issue_id'], "Version:",
272+
incident['issue_version'])
273+
274+
275+
if __name__ == "__main__":
276+
main()

0 commit comments

Comments
 (0)