Skip to content

Commit a6d0d7b

Browse files
committed
CI - restyle script summary and annotations in PRs
similar to .ino builder, prepare a short 'diff' summary of all the requried changes https://docs.github.com/en/actions/using-workflows/workflow-commands-for-github-actions#adding-a-job-summary provide inline annotations, so it is apparent clang-format job is the cause of the PR actions check failure https://docs.github.com/en/actions/using-workflows/workflow-commands-for-github-actions#setting-a-notice-message
1 parent 27272de commit a6d0d7b

File tree

2 files changed

+279
-47
lines changed

2 files changed

+279
-47
lines changed

tests/restyle.py

Lines changed: 270 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,270 @@
1+
#!/usr/bin/env python
2+
3+
import argparse
4+
import os
5+
import sys
6+
import pathlib
7+
import subprocess
8+
import contextlib
9+
10+
from dataclasses import dataclass
11+
12+
13+
GIT_ROOT = pathlib.Path(
14+
subprocess.check_output(
15+
["git", "rev-parse", "--show-toplevel"], universal_newlines=True
16+
).strip()
17+
)
18+
19+
20+
def clang_format(clang_format, config, files):
21+
if not files:
22+
raise ValueError("Files list cannot be empty")
23+
24+
cmd = [clang_format, "--verbose", f"--style=file:{config.as_posix()}", "-i"]
25+
cmd.extend(files)
26+
27+
subprocess.run(cmd, check=True)
28+
29+
30+
def ls_files(patterns):
31+
"""Git-only search, but rather poor at matching complex patterns (at least w/ <=py3.12)"""
32+
proc = subprocess.run(
33+
["git", "--no-pager", "ls-files"],
34+
capture_output=True,
35+
check=True,
36+
universal_newlines=True,
37+
)
38+
39+
out = []
40+
for line in proc.stdout.split("\n"):
41+
path = pathlib.Path(line.strip())
42+
if any(path.match(pattern) for pattern in patterns):
43+
out.append(path)
44+
45+
return out
46+
47+
48+
def find_files(patterns):
49+
"""Filesystem search, matches both git and non-git files"""
50+
return [
51+
file
52+
for pattern in patterns
53+
for file in [found for found in GIT_ROOT.rglob(pattern)]
54+
]
55+
56+
57+
def find_core_files():
58+
"""Returns a subset of Core files that should be formatted"""
59+
return [
60+
file
61+
for file in find_files(
62+
(
63+
"cores/esp8266/Lwip*",
64+
"libraries/ESP8266mDNS/**/*",
65+
"libraries/Wire/**/*",
66+
"libraries/lwIP*/**/*",
67+
"cores/esp8266/debug*",
68+
"cores/esp8266/core_esp8266_si2c*",
69+
"cores/esp8266/StreamString*",
70+
"cores/esp8266/StreamSend*",
71+
"libraries/Netdump/**/*",
72+
"tests/**/*",
73+
)
74+
)
75+
if file.is_file()
76+
and file.suffix in (".c", ".cpp", ".h", ".hpp")
77+
and not GIT_ROOT / "tests/host/bin" in file.parents
78+
and not GIT_ROOT / "tests/host/common/catch.hpp" == file
79+
]
80+
81+
82+
def find_arduino_files():
83+
"""Returns every .ino file available in the repository, excluding submodule ones"""
84+
return [
85+
ino
86+
for library in find_files(("libraries/*",))
87+
if library.is_dir() and not (library / ".git").exists()
88+
for ino in library.rglob("**/*.ino")
89+
]
90+
91+
92+
FILES_PRESETS = {
93+
"core": find_core_files,
94+
"arduino": find_arduino_files,
95+
}
96+
97+
98+
@dataclass
99+
class Changed:
100+
file: str
101+
hunk: list[str]
102+
lines: list[int]
103+
104+
105+
class Context:
106+
def __init__(self):
107+
self.append_hunk = False
108+
self.deleted = False
109+
self.file = ""
110+
self.hunk = []
111+
self.markers = []
112+
113+
def reset(self):
114+
self.__init__()
115+
116+
def reset_with_line(self, line):
117+
self.reset()
118+
self.hunk.append(line)
119+
120+
def pop(self, out, line):
121+
if self.file and self.hunk and self.markers:
122+
out.append(
123+
Changed(file=self.file, hunk="\n".join(self.hunk), lines=self.markers)
124+
)
125+
126+
self.reset_with_line(line)
127+
128+
129+
def changed_files():
130+
"""
131+
Naive git-diff output parser. Generates list[Changed] for every file changed after clang-format.
132+
"""
133+
proc = subprocess.run(
134+
["git", "--no-pager", "diff"],
135+
capture_output=True,
136+
check=True,
137+
universal_newlines=True,
138+
)
139+
140+
ctx = Context()
141+
out = []
142+
143+
# TODO: pygit2?
144+
# ref. https://github.com/cpp-linter/cpp-linter/blob/main/cpp_linter/git/__init__.py ::parse_diff
145+
# ref. https://github.com/libgit2/pygit2/blob/master/src/diff.c ::parse_diff
146+
for line in proc.stdout.split("\n"):
147+
# '--- a/path/to/changed/file' most likely
148+
# '--- a/dev/null' aka created file. should be ignored, same as removed ones
149+
if line.startswith("---"):
150+
ctx.pop(out, line)
151+
ctx.deleted = "/dev/null" in file
152+
153+
# '+++ b/path/to/changed/file' most likely
154+
# '+++ /dev/null' aka removed file
155+
elif not ctx.deleted and line.startswith("+++"):
156+
ctx.hunk.append(line)
157+
158+
_, file = line.split(" ")
159+
ctx.deleted = "/dev/null" in file
160+
if not ctx.deleted:
161+
ctx.file = file[2:]
162+
163+
# @@ from-file-line-numbers to-file-line-numbers @@
164+
elif not ctx.deleted and line.startswith("@@"):
165+
ctx.hunk.append(line)
166+
167+
_, _, numbers, _ = line.split(" ", 3)
168+
if "," in numbers:
169+
numbers, _ = numbers.split(",") # drop count
170+
171+
numbers = numbers.replace("+", "")
172+
numbers = numbers.replace("-", "")
173+
174+
ctx.markers.append(int(numbers))
175+
ctx.append_hunk = True
176+
177+
# capture diff for the summary
178+
elif ctx.append_hunk and line.startswith(("+", "-", " ")):
179+
ctx.hunk.append(line)
180+
181+
ctx.pop(out, line)
182+
183+
return out
184+
185+
186+
def errors_changed(changed: Changed):
187+
all_lines = ", ".join(str(x) for x in changed.lines)
188+
for line in changed.lines:
189+
print(
190+
f"::error file={changed.file},title=Run tests/restyle.sh and re-commit {changed.file},line={line}::File {changed.file} failed clang-format style check. (lines {all_lines})"
191+
)
192+
193+
194+
SUMMARY_PATH = pathlib.Path(os.environ.get("GITHUB_STEP_SUMMARY", os.devnull))
195+
SUMMARY_OUTPUT = SUMMARY_PATH.open("a")
196+
197+
198+
def summary_diff(changed: Changed):
199+
with contextlib.redirect_stdout(SUMMARY_OUTPUT):
200+
print(f"# {changed.file} (suggested change)")
201+
print("```diff")
202+
print(changed.hunk)
203+
print("```")
204+
205+
206+
def stdout_diff():
207+
subprocess.run(["git", "--no-pager", "diff"])
208+
209+
210+
def assert_unchanged():
211+
subprocess.run(
212+
["git", "diff", "--exit-code"], check=True, stdout=subprocess.DEVNULL
213+
)
214+
215+
216+
def run_format(args):
217+
targets = []
218+
219+
for include in args.include:
220+
targets.append(
221+
(GIT_ROOT / f"tests/clang-format-{include}.yaml", FILES_PRESETS[include]())
222+
)
223+
224+
if not targets:
225+
targets.append((args.config, args.files))
226+
227+
for target in targets:
228+
clang_format(args.clang_format, *target)
229+
230+
231+
def run_assert(args):
232+
for changed in changed_files():
233+
if args.with_errors:
234+
errors_changed(changed)
235+
if args.with_summary:
236+
summary_diff(changed)
237+
238+
if args.with_diff:
239+
stdout_diff()
240+
241+
assert_unchanged()
242+
243+
244+
if __name__ == "__main__":
245+
parser = argparse.ArgumentParser()
246+
247+
cmd = parser.add_subparsers(required=True)
248+
format_ = cmd.add_parser("format")
249+
format_.set_defaults(func=run_format)
250+
format_.add_argument("--clang-format", default="clang-format")
251+
252+
fmt = format_.add_subparsers(required=True)
253+
254+
preset = fmt.add_parser("preset")
255+
preset.add_argument(
256+
"--include", action="append", required=True, choices=tuple(FILES_PRESETS.keys())
257+
)
258+
259+
files = fmt.add_parser("files")
260+
files.add_argument("--config", type=pathlib.Path, required=True)
261+
files.add_argument("files", type=pathlib.Path, nargs="+")
262+
263+
assert_ = cmd.add_parser("assert")
264+
assert_.set_defaults(func=run_assert)
265+
assert_.add_argument("--with-diff", action="store_true")
266+
assert_.add_argument("--with-errors", action="store_true")
267+
assert_.add_argument("--with-summary", action="store_true")
268+
269+
args = parser.parse_args()
270+
args.func(args)

tests/restyle.sh

Lines changed: 9 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
#!/bin/sh
2-
# requires clang-format, git, python3 with pyyaml
2+
# requires python3, git, and runnable clang-format (specified below)
33

44
set -e -x
55

@@ -11,51 +11,13 @@ test -d ${root}/libraries
1111
# default to v15, latest stable version from ubuntu-latest Github Actions image
1212
CLANG_FORMAT=${CLANG_FORMAT:-clang-format-15}
1313

14-
#########################################
15-
# 'all' variable should be "cores/esp8266 libraries"
16-
17-
all=${1:-"
18-
cores/esp8266/Lwip*
19-
libraries/ESP8266mDNS
20-
libraries/Wire
21-
libraries/lwIP*
22-
cores/esp8266/debug*
23-
cores/esp8266/core_esp8266_si2c.cpp
24-
cores/esp8266/StreamString.*
25-
cores/esp8266/StreamSend.*
26-
libraries/Netdump
27-
tests
28-
"}
29-
30-
#########################################
31-
# restyling core & libraries
32-
3314
cd $root
34-
35-
style=${root}/tests/clang-format-core.yaml
36-
for target in $all; do
37-
if [ -d "$target" ]; then
38-
find $target \
39-
'(' -name "*.cpp" -o -name "*.c" -o -name "*.h" ')' \
40-
-exec $CLANG_FORMAT --verbose --style="file:$style" -i {} \;
41-
else
42-
$CLANG_FORMAT --verbose --style="file:$style" -i $target
43-
fi
44-
done
45-
46-
#########################################
47-
# restyling arduino examples
48-
49-
# TODO should not be matched, these are formatted externally
50-
# exclude=$(git submodule --quiet foreach git rev-parse --show-toplevel | grep libraries)
51-
52-
if [ -z $1 ] ; then
53-
style=${root}/tests/clang-format-arduino.yaml
54-
find libraries \
55-
-path libraries/ESP8266SdFat -prune -o \
56-
-path libraries/Ethernet -prune -o \
57-
-path libraries/SoftwareSerial -prune -o \
58-
-name '*.ino' -exec $CLANG_FORMAT --verbose --style="file:$style" -i {} \;
15+
python $root/tests/restyle.py format --clang-format=$CLANG_FORMAT preset --include core --include arduino
16+
17+
if [ $CI = "true" ] ; then
18+
echo foo
19+
python $root/tests/restyle.py assert --with-summary --with-errors
20+
else
21+
echo bar
22+
python $root/tests/restyle.py assert --with-diff
5923
fi
60-
61-
#########################################

0 commit comments

Comments
 (0)