Skip to content

Commit 30fd704

Browse files
committed
Merge branch 'master' into linting
* master: Use task queue to spawn multiple processes of tidy
2 parents 1ad1991 + 88d4e32 commit 30fd704

File tree

4 files changed

+160
-58
lines changed

4 files changed

+160
-58
lines changed

action.yml

+5
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,10 @@ inputs:
6161
description: "Use annotations instead of comments. See README for limitations on annotations"
6262
required: false
6363
default: false
64+
parallel:
65+
description: "Number of tidy instances to be run in parallel. Zero will automatically determine the right number."
66+
required: false
67+
default: "0"
6468
pr:
6569
default: ${{ github.event.pull_request.number }}
6670
repo:
@@ -88,3 +92,4 @@ runs:
8892
- --lgtm-comment-body='${{ inputs.lgtm_comment_body }}'
8993
- --split_workflow=${{ inputs.split_workflow }}
9094
- --annotations=${{ inputs.annotations }}
95+
- --parallel=${{ inputs.parallel }}

post/clang_tidy_review/clang_tidy_review/__init__.py

+139-52
Original file line numberDiff line numberDiff line change
@@ -6,16 +6,25 @@
66
import argparse
77
import base64
88
import contextlib
9+
import datetime
910
import fnmatch
11+
import glob
1012
import io
1113
import itertools
1214
import json
15+
import multiprocessing
1316
import os
1417
import pathlib
1518
import pprint
19+
import queue
1620
import re
21+
import shutil
1722
import subprocess
23+
import sys
24+
import tempfile
1825
import textwrap
26+
import threading
27+
import traceback
1928
import zipfile
2029
from operator import itemgetter
2130
from pathlib import Path
@@ -150,50 +159,48 @@ def get_auth_from_arguments(args: argparse.Namespace) -> Auth.Auth:
150159

151160

152161
def build_clang_tidy_warnings(
153-
line_filter,
154-
build_dir,
155-
clang_tidy_checks,
156-
clang_tidy_binary: pathlib.Path,
157-
config_file,
158-
files,
159-
username: str,
162+
base_invocation: List,
163+
env: dict,
164+
tmpdir: str,
165+
task_queue: queue.Queue,
166+
lock: threading.Lock,
167+
failed_files: List,
160168
) -> None:
161-
"""Run clang-tidy on the given files and save output into FIXES_FILE"""
169+
"""Run clang-tidy on the given files and save output into a temporary file"""
162170

163-
config = config_file_or_checks(clang_tidy_binary, clang_tidy_checks, config_file)
171+
while True:
172+
name = task_queue.get()
173+
invocation = base_invocation[:]
164174

165-
args = [
166-
clang_tidy_binary,
167-
f"-p={build_dir}",
168-
f"-line-filter={line_filter}",
169-
f"--export-fixes={FIXES_FILE}",
170-
"--enable-check-profile",
171-
f"-store-check-profile={PROFILE_DIR}",
172-
]
175+
# Get a temporary file. We immediately close the handle so clang-tidy can
176+
# overwrite it.
177+
(handle, fixes_file) = tempfile.mkstemp(suffix=".yaml", dir=tmpdir)
178+
os.close(handle)
179+
invocation.append(f"--export-fixes={fixes_file}")
173180

174-
if config:
175-
print(f"Using config: {config}")
176-
args.append(config)
177-
else:
178-
print("Using recursive directory config")
181+
invocation.append(name)
179182

180-
args += files
181-
182-
try:
183-
with message_group(f"Running:\n\t{args}"):
184-
env = dict(os.environ)
185-
env["USER"] = username
186-
subprocess.run(
187-
args,
188-
capture_output=True,
189-
check=True,
190-
encoding="utf-8",
191-
env=env,
192-
)
193-
except subprocess.CalledProcessError as e:
194-
print(
195-
f"\n\nclang-tidy failed with return code {e.returncode} and error:\n{e.stderr}\nOutput was:\n{e.stdout}"
183+
proc = subprocess.Popen(
184+
invocation, stdout=subprocess.PIPE, stderr=subprocess.PIPE, env=env
196185
)
186+
output, err = proc.communicate()
187+
end = datetime.datetime.now()
188+
189+
if proc.returncode != 0:
190+
if proc.returncode < 0:
191+
msg = f"{name}: terminated by signal {-proc.returncode}\n"
192+
err += msg.encode("utf-8")
193+
failed_files.append(name)
194+
with lock:
195+
subprocess.list2cmdline(invocation)
196+
sys.stdout.write(
197+
f'{name}: {subprocess.list2cmdline(invocation)}\n{output.decode("utf-8")}'
198+
)
199+
if len(err) > 0:
200+
sys.stdout.flush()
201+
sys.stderr.write(err.decode("utf-8"))
202+
203+
task_queue.task_done()
197204

198205

199206
def clang_tidy_version(clang_tidy_binary: pathlib.Path):
@@ -239,8 +246,30 @@ def config_file_or_checks(
239246
return "--config"
240247

241248

242-
def load_clang_tidy_warnings():
243-
"""Read clang-tidy warnings from FIXES_FILE. Can be produced by build_clang_tidy_warnings"""
249+
def merge_replacement_files(tmpdir: str, mergefile: str):
250+
"""Merge all replacement files in a directory into a single file"""
251+
# The fixes suggested by clang-tidy >= 4.0.0 are given under
252+
# the top level key 'Diagnostics' in the output yaml files
253+
mergekey = "Diagnostics"
254+
merged = []
255+
for replacefile in glob.iglob(os.path.join(tmpdir, "*.yaml")):
256+
content = yaml.safe_load(open(replacefile, "r"))
257+
if not content:
258+
continue # Skip empty files.
259+
merged.extend(content.get(mergekey, []))
260+
261+
if merged:
262+
# MainSourceFile: The key is required by the definition inside
263+
# include/clang/Tooling/ReplacementsYaml.h, but the value
264+
# is actually never used inside clang-apply-replacements,
265+
# so we set it to '' here.
266+
output = {"MainSourceFile": "", mergekey: merged}
267+
with open(mergefile, "w") as out:
268+
yaml.safe_dump(output, out)
269+
270+
271+
def load_clang_tidy_warnings(fixes_file) -> Dict:
272+
"""Read clang-tidy warnings from fixes_file. Can be produced by build_clang_tidy_warnings"""
244273
try:
245274
with Path(FIXES_FILE).open() as fixes_file:
246275
return yaml.safe_load(fixes_file)
@@ -807,7 +836,9 @@ def create_review_file(
807836
return review
808837

809838

810-
def make_timing_summary(clang_tidy_profiling: Dict, sha: Optional[str] = None) -> str:
839+
def make_timing_summary(
840+
clang_tidy_profiling: Dict, real_time: datetime.timedelta, sha: Optional[str] = None
841+
) -> str:
811842
if not clang_tidy_profiling:
812843
return ""
813844
top_amount = 10
@@ -884,7 +915,9 @@ def make_timing_summary(clang_tidy_profiling: Dict, sha: Optional[str] = None) -
884915
c = decorate_check_names(f"[{c}]").replace("[[", "[").rstrip("]")
885916
check_summary += f"|{c}|{u:.2f}|{s:.2f}|{w:.2f}|\n"
886917

887-
return f"## Timing\n{file_summary}{check_summary}"
918+
return (
919+
f"## Timing\nReal time: {real_time.seconds:.2f}\n{file_summary}{check_summary}"
920+
)
888921

889922

890923
def filter_files(diff, include: List[str], exclude: List[str]) -> List:
@@ -906,6 +939,7 @@ def create_review(
906939
clang_tidy_checks: str,
907940
clang_tidy_binary: pathlib.Path,
908941
config_file: str,
942+
max_task: int,
909943
include: List[str],
910944
exclude: List[str],
911945
) -> Optional[PRReview]:
@@ -914,6 +948,9 @@ def create_review(
914948
915949
"""
916950

951+
if max_task == 0:
952+
max_task = multiprocessing.cpu_count()
953+
917954
diff = pull_request.get_pr_diff()
918955
print(f"\nDiff from GitHub PR:\n{diff}\n")
919956

@@ -955,18 +992,68 @@ def create_review(
955992
username = pull_request.get_pr_author() or "your name here"
956993

957994
# Run clang-tidy with the configured parameters and produce the CLANG_TIDY_FIXES file
958-
build_clang_tidy_warnings(
959-
line_ranges,
960-
build_dir,
961-
clang_tidy_checks,
995+
return_code = 0
996+
export_fixes_dir = tempfile.mkdtemp()
997+
env = dict(os.environ, USER=username)
998+
config = config_file_or_checks(clang_tidy_binary, clang_tidy_checks, config_file)
999+
base_invocation = [
9621000
clang_tidy_binary,
963-
config_file,
964-
files,
965-
username,
966-
)
1001+
f"-p={build_dir}",
1002+
f"-line-filter={line_ranges}",
1003+
"--enable-check-profile",
1004+
f"-store-check-profile={PROFILE_DIR}",
1005+
]
1006+
if config:
1007+
print(f"Using config: {config}")
1008+
base_invocation.append(config)
1009+
else:
1010+
print("Using recursive directory config")
1011+
1012+
print(f"Spawning a task queue with {max_task} processes")
1013+
start = datetime.datetime.now()
1014+
try:
1015+
# Spin up a bunch of tidy-launching threads.
1016+
task_queue = queue.Queue(max_task)
1017+
# List of files with a non-zero return code.
1018+
failed_files = []
1019+
lock = threading.Lock()
1020+
for _ in range(max_task):
1021+
t = threading.Thread(
1022+
target=build_clang_tidy_warnings,
1023+
args=(
1024+
base_invocation,
1025+
env,
1026+
export_fixes_dir,
1027+
task_queue,
1028+
lock,
1029+
failed_files,
1030+
),
1031+
)
1032+
t.daemon = True
1033+
t.start()
1034+
1035+
# Fill the queue with files.
1036+
for name in files:
1037+
task_queue.put(name)
1038+
1039+
# Wait for all threads to be done.
1040+
task_queue.join()
1041+
if len(failed_files):
1042+
return_code = 1
1043+
1044+
except KeyboardInterrupt:
1045+
# This is a sad hack. Unfortunately subprocess goes
1046+
# bonkers with ctrl-c and we start forking merrily.
1047+
print("\nCtrl-C detected, goodbye.")
1048+
os.kill(0, 9)
1049+
raise
1050+
real_duration = datetime.datetime.now() - start
9671051

9681052
# Read and parse the CLANG_TIDY_FIXES file
969-
clang_tidy_warnings = load_clang_tidy_warnings()
1053+
print("Writing fixes to " + FIXES_FILE + " ...")
1054+
merge_replacement_files(export_fixes_dir, FIXES_FILE)
1055+
shutil.rmtree(export_fixes_dir)
1056+
clang_tidy_warnings = load_clang_tidy_warnings(FIXES_FILE)
9701057

9711058
# Read and parse the timing data
9721059
clang_tidy_profiling = load_and_merge_profiling()
@@ -977,7 +1064,7 @@ def create_review(
9771064
sha = os.environ.get("GITHUB_SHA")
9781065

9791066
# Post to the action job summary
980-
step_summary = make_timing_summary(clang_tidy_profiling, sha)
1067+
step_summary = make_timing_summary(clang_tidy_profiling, real_duration, sha)
9811068
set_summary(step_summary)
9821069

9831070
print("clang-tidy had the following warnings:\n", clang_tidy_warnings, flush=True)

post/clang_tidy_review/clang_tidy_review/review.py

+8
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,13 @@ def main():
110110
type=bool_argument,
111111
default=False,
112112
)
113+
parser.add_argument(
114+
"-j",
115+
"--parallel",
116+
help="Number of tidy instances to be run in parallel.",
117+
type=int,
118+
default=0,
119+
)
113120
parser.add_argument(
114121
"--dry-run", help="Run and generate review, but don't post", action="store_true"
115122
)
@@ -155,6 +162,7 @@ def main():
155162
args.clang_tidy_checks,
156163
args.clang_tidy_binary,
157164
args.config_file,
165+
args.parallel,
158166
include,
159167
exclude,
160168
)

tests/test_review.py

+8-6
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import datetime
2+
13
import clang_tidy_review as ctr
24

35
import difflib
@@ -235,10 +237,10 @@ def test_line_ranges():
235237
assert line_ranges == expected_line_ranges
236238

237239

238-
def test_load_clang_tidy_warnings(monkeypatch):
239-
monkeypatch.setattr(ctr, "FIXES_FILE", str(TEST_DIR / f"src/test_{ctr.FIXES_FILE}"))
240-
241-
warnings = ctr.load_clang_tidy_warnings()
240+
def test_load_clang_tidy_warnings():
241+
warnings = ctr.load_clang_tidy_warnings(
242+
str(TEST_DIR / f"src/test_{ctr.FIXES_FILE}")
243+
)
242244

243245
assert sorted(list(warnings.keys())) == ["Diagnostics", "MainSourceFile"]
244246
assert warnings["MainSourceFile"] == "/clang_tidy_review/src/hello.cxx"
@@ -470,5 +472,5 @@ def test_timing_summary(monkeypatch):
470472
assert "time.clang-tidy.total.wall" in profiling["hello.cxx"].keys()
471473
assert "time.clang-tidy.total.user" in profiling["hello.cxx"].keys()
472474
assert "time.clang-tidy.total.sys" in profiling["hello.cxx"].keys()
473-
summary = ctr.make_timing_summary(profiling)
474-
assert len(summary.split("\n")) == 21
475+
summary = ctr.make_timing_summary(profiling, datetime.timedelta(seconds=42))
476+
assert len(summary.split("\n")) == 22

0 commit comments

Comments
 (0)