Skip to content

Commit 1265c84

Browse files
committed
ci: generate source file/test db
Signed-off-by: Anas Nashif <[email protected]>
1 parent f49a13c commit 1265c84

File tree

3 files changed

+156
-0
lines changed

3 files changed

+156
-0
lines changed

scripts/ci/db/find_test_combo.py

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
#!/usr/bin/env python3
2+
3+
import json
4+
import os
5+
import re
6+
import sys
7+
from pathlib import Path
8+
from collections import defaultdict
9+
import argparse
10+
11+
if "ZEPHYR_BASE" not in os.environ:
12+
exit("$ZEPHYR_BASE environment variable undefined.")
13+
14+
# These are globally used variables. They are assigned in __main__ and are visible in further methods
15+
# however, pylint complains that it doesn't recognize them when used (used-before-assignment).
16+
zephyr_base = Path(os.environ['ZEPHYR_BASE'])
17+
18+
sys.path.insert(0, os.path.join(zephyr_base / "scripts"))
19+
from get_maintainer import Maintainers
20+
21+
def load_database(database_path):
22+
with open(database_path, 'r') as file:
23+
return json.load(file)
24+
25+
def find_areas(files):
26+
maintf = zephyr_base / "MAINTAINERS.yml"
27+
maintainer_file = Maintainers(maintf)
28+
29+
num_files = 0
30+
all_areas = set()
31+
32+
for changed_file in files:
33+
num_files += 1
34+
print(f"file: {changed_file}")
35+
areas = maintainer_file.path2areas(changed_file)
36+
37+
if not areas:
38+
continue
39+
all_areas.update(areas)
40+
tests = []
41+
for area in all_areas:
42+
for suite in area.tests:
43+
tests.append(f"{suite}.*")
44+
return tests
45+
46+
def find_best_coverage(database, changed_files, tests):
47+
coverage = defaultdict(lambda: defaultdict(int))
48+
49+
for file in changed_files:
50+
if file in database:
51+
for entry in database[file]:
52+
if not any(re.search(f"^{test}", entry["testsuite_id"]) for test in tests):
53+
print("skip")
54+
continue
55+
testsuite_id = entry["testsuite_id"]
56+
platform = entry["platform"]
57+
coverage[testsuite_id][platform] += 1
58+
59+
best_coverage = []
60+
for testsuite_id, platforms in coverage.items():
61+
for platform, count in platforms.items():
62+
best_coverage.append((testsuite_id, platform, count))
63+
64+
best_coverage.sort(key=lambda x: x[2], reverse=True)
65+
return best_coverage
66+
67+
def main(database_path, changed_files):
68+
tests = find_areas(changed_files)
69+
database = load_database(database_path)
70+
best_coverage = find_best_coverage(database, changed_files, tests)
71+
72+
if best_coverage:
73+
print("Best coverage testsuites and platforms:")
74+
for testsuite_id, platform, count in best_coverage:
75+
print(f"Testsuite: {testsuite_id}, Platform: {platform}, Coverage: {count}")
76+
else:
77+
print("No matching testsuites found for the provided files.")
78+
79+
80+
if __name__ == "__main__":
81+
parser = argparse.ArgumentParser(description="Find the best coverage testsuites and platforms for changed files.")
82+
parser.add_argument("--database", help="Path to the testsuite database JSON file")
83+
parser.add_argument("--changed-files", action="append", help="List of changed files")
84+
85+
args = parser.parse_args()
86+
87+
main(args.database, args.changed_files)

scripts/ci/db/gen_test_database.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
#!/usr/bin/env python3
2+
import os
3+
import json
4+
import argparse
5+
6+
def find_compile_commands_files(directory):
7+
compile_commands_files = []
8+
for root, _, files in os.walk(directory):
9+
for file in files:
10+
if file.startswith("compile_commands_") and file.endswith(".json"):
11+
compile_commands_files.append(os.path.join(root, file))
12+
return compile_commands_files
13+
14+
def generate_testsuite_database(directory, output_file):
15+
compile_commands_files = find_compile_commands_files(directory)
16+
database = {}
17+
18+
for file_path in compile_commands_files:
19+
with open(file_path, 'r') as file:
20+
data = json.load(file)
21+
testsuite_id = data.get("testsuite_id")
22+
platform = data.get("platform")
23+
files = data.get("files", [])
24+
25+
for file in files:
26+
if file not in database:
27+
database[file] = []
28+
database[file].append({"testsuite_id": testsuite_id, "platform": platform})
29+
30+
with open(output_file, 'w') as output:
31+
json.dump(database, output, indent=4)
32+
33+
if __name__ == "__main__":
34+
parser = argparse.ArgumentParser(description="Generate a testsuite database from compile_commands files.")
35+
parser.add_argument("--directory", required=True, help="Directory to search for compile_commands files")
36+
parser.add_argument("--output", required=True, help="Output file for the generated database")
37+
args = parser.parse_args()
38+
39+
generate_testsuite_database(args.directory, args.output)
40+
print(f"Database generated and saved to {args.output}")

scripts/pylib/twister/twisterlib/runner.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@
1515
import subprocess
1616
import sys
1717
import time
18+
import json
19+
import hashlib
1820
import traceback
1921
from math import log10
2022
from multiprocessing import Lock, Process, Value
@@ -1013,6 +1015,33 @@ def process(self, pipeline, done, message, lock, results):
10131015
elif op == "cmake":
10141016
try:
10151017
ret = self.cmake()
1018+
compile_commands_path = os.path.join(self.instance.build_dir,
1019+
'compile_commands.json')
1020+
if os.path.exists(compile_commands_path):
1021+
with open(compile_commands_path, 'r') as f:
1022+
compile_commands = json.load(f)
1023+
1024+
# Extract only the file names from the compile commands
1025+
file_names = [entry['file'] for entry in compile_commands]
1026+
1027+
# Create a unique filename using a SHA hash based on the testsuite identifier and platform
1028+
unique_id = f"{self.instance.testsuite.id}_{self.instance.platform.name}"
1029+
sha_hash = hashlib.sha256(unique_id.encode()).hexdigest()
1030+
instance_file = os.path.join(self.options.outdir, f'compile_commands_{sha_hash}.json')
1031+
1032+
# Add testsuite id and platform to the data
1033+
file_names_relative = list(set([
1034+
os.path.relpath(file, start=ZEPHYR_BASE) for file in file_names
1035+
if not file.startswith(self.options.outdir)
1036+
]))
1037+
data = {
1038+
"testsuite_id": self.instance.testsuite.id,
1039+
"platform": self.instance.platform.name,
1040+
"files": file_names_relative
1041+
}
1042+
1043+
with open(instance_file, 'w') as f:
1044+
json.dump(data, f, indent=4)
10161045
if self.instance.status in [TwisterStatus.FAIL, TwisterStatus.ERROR]:
10171046
next_op = 'report'
10181047
elif self.options.cmake_only:

0 commit comments

Comments
 (0)