Skip to content

Commit 3150647

Browse files
committed
Add filtering to ament_clang_tidy
This change adds two types of filtering to which files are tested with clang-tidy. The motivation behind this is clang-tidy itself can take a really long time to execute and as a result, we'd like to limit the number of files it tests to those changed in a given PR for CI for moveit. This adds three arguments to ament_clang_tidy: The first one allows filtering the packages tested to a single repo. This is useful in CI to limit the tests to the given repo you are executing on. Also, if it finds a .clang-tidy config file in the root directory of the repo it will use that instead of the default clang-tidy config. **known issue**: the implementation of this depends on on `catkin_topological_order`. Is there an ament/ros2 friendly tool for doing the same thing? This one uses git to compare the current state of the repo with some previous version and test only the files changed. This enables us to use clang-tidy in ci with large projects like moveit because it limits the scope of the clang-tidy test to the packages that were built. This depends on the `filter-repo-path` feature above because the git commands used to figure out what files changed need to be run from the repo directory. I feel the least strong about this feature. I added this because I wanted it while debugging what clang-tidy command was actually being executed. I think this could be a useful argument in general for python scripts that execute external programs. I'm working to improve the ci used by moveit and moveit2. I wrote this change to help with the migration away from travis we are going through with moveit (hence the catkin dependency). Because this script is not easily available in ros1 (asfaik) I have this PR to submit the contents of it with this change into a tools directory in moveit for our own use (moveit/moveit#2470). I'd appreciate advice if there is a better approach I should use to get ament scripts that are useful on ros1 projects. Signed-off-by: Tyler Weaver <[email protected]>
1 parent 8bf194a commit 3150647

File tree

1 file changed

+108
-2
lines changed
  • ament_clang_tidy/ament_clang_tidy

1 file changed

+108
-2
lines changed

ament_clang_tidy/ament_clang_tidy/main.py

Lines changed: 108 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -80,21 +80,53 @@ def main(argv=sys.argv[1:]):
8080
parser.add_argument(
8181
'--xunit-file',
8282
help='Generate a xunit compliant XML file')
83+
84+
parser.add_argument(
85+
'--filter-repo-path',
86+
help='Used to test only packages from one repo. '
87+
'Accepts a path to repo containing ros packages. If this path contains '
88+
'a .clang-tidy config file, the tests will use that config.')
89+
parser.add_argument(
90+
'--filter-diff',
91+
help='Used to only run tests on packages with changes since a specified git commit. '
92+
'Accepts branch, tag, or git commit hash to generate diff. Requires --filter-repo-path.')
93+
94+
parser.add_argument(
95+
'--verbose',
96+
action='store_true',
97+
help='Show clang-tidy commands as they are run.')
8398
args = parser.parse_args(argv)
8499

100+
if args.filter_repo_path:
101+
repo_clang_tidy = args.filter_repo_path + '/.clang-tidy'
102+
if os.path.exists(repo_clang_tidy):
103+
args.config_file = repo_clang_tidy
104+
85105
if not os.path.exists(args.config_file):
86106
print("Could not find config file '%s'" % args.config_file,
87107
file=sys.stderr)
88108
return 1
89109

110+
print('Using config file %s' % args.config_file)
111+
90112
if args.xunit_file:
91113
start_time = time.time()
92114

93115
compilation_dbs = get_compilation_db_files(args.paths)
94116
if not compilation_dbs:
95-
print('No compilation database files found', file=sys.stderr)
117+
print('No compilation database files found in paths: %s' % (args.paths), file=sys.stderr)
96118
return 1
97119

120+
if args.filter_repo_path:
121+
compilation_dbs = filter_packages_in_repo(compilation_dbs, args.filter_repo_path)
122+
123+
if args.filter_diff:
124+
if not args.filter_repo_path:
125+
print('Diff filter requires --filter-repo-path to be set.', file=sys.stderr)
126+
return 1
127+
compilation_dbs = filter_diff(compilation_dbs, args.filter_repo_path,
128+
args.filter_diff)
129+
98130
bin_names = [
99131
# 'clang-tidy',
100132
'clang-tidy-6.0',
@@ -157,6 +189,9 @@ def is_unittest_source(package, file_path):
157189

158190
files.append(item['file'])
159191
full_cmd = cmd + [item['file']]
192+
193+
if args.verbose:
194+
print(' '.join(full_cmd))
160195
try:
161196
output += subprocess.check_output(full_cmd,
162197
stderr=subprocess.DEVNULL).strip().decode()
@@ -171,7 +206,7 @@ def is_unittest_source(package, file_path):
171206
for compilation_db in compilation_dbs:
172207
package_dir = os.path.dirname(compilation_db)
173208
package_name = os.path.basename(package_dir)
174-
print('found compilation database for package "%s"...' % package_name)
209+
print('Invoking clang-tidy for package `%s`...' % package_name)
175210
(source_files, output) = invoke_clang_tidy(compilation_db)
176211
files += source_files
177212
outputs.append(output)
@@ -259,6 +294,77 @@ def get_compilation_db_files(paths):
259294
return [os.path.normpath(f) for f in files]
260295

261296

297+
def filter_packages_in_repo(compilation_dbs, repo_path):
298+
bin_names = [
299+
'catkin_topological_order',
300+
]
301+
catkin_topological_order_bin = find_executable(bin_names)
302+
if not catkin_topological_order_bin:
303+
print('Could not find %s executable' %
304+
' / '.join(["'%s'" % n for n in bin_names]), file=sys.stderr)
305+
return 1
306+
307+
cmd = [catkin_topological_order_bin, repo_path, '--only-names']
308+
output = ''
309+
try:
310+
output = subprocess.check_output(cmd).strip().decode()
311+
except subprocess.CalledProcessError as e:
312+
print('The invocation of "%s" failed with error code %d: %s' %
313+
(os.path.basename(catkin_topological_order_bin), e.returncode, e),
314+
file=sys.stderr)
315+
316+
def test(path):
317+
return os.path.basename(os.path.dirname(path)) in output.split()
318+
319+
filtered_dbs = list(filter(test, compilation_dbs))
320+
return filtered_dbs
321+
322+
323+
def filter_diff(compilation_dbs, repo_path, diff_head):
324+
bin_names = [
325+
'git',
326+
]
327+
git_bin = find_executable(bin_names)
328+
if not git_bin:
329+
print('Could not find %s executable' %
330+
' / '.join(["'%s'" % n for n in bin_names]), file=sys.stderr)
331+
return 1
332+
333+
def modified_files_test(db_path):
334+
package_dir = os.path.dirname(db_path)
335+
package_name = os.path.basename(package_dir)
336+
src_path = ''
337+
with open(package_dir + '/CMakeCache.txt', 'r') as file:
338+
for line in file:
339+
if re.match('^CMAKE_HOME_DIRECTORY:INTERNAL=', line):
340+
src_path = line.split('=')[1].strip()
341+
break
342+
343+
if len(src_path) == 0:
344+
print('Source path not found in CMakeCache.txt of package '
345+
'`%s`, skipping.' % package_name, file=sys.stderr)
346+
return False
347+
348+
cmd = ['git', 'diff', '--name-only', '--diff-filter=MA', diff_head + '..HEAD', src_path]
349+
output = ''
350+
try:
351+
output = subprocess.check_output(cmd, cwd=repo_path).strip().decode()
352+
except subprocess.CalledProcessError as e:
353+
print('The invocation of "%s" failed with error code %d: %s' %
354+
(os.path.basename(git_bin), e.returncode, ' '.join(cmd)),
355+
file=sys.stderr)
356+
return False
357+
modified_files = output.split()
358+
if len(modified_files) == 0:
359+
print('No files modified in package `%s`, skipping clang-tidy test.' % package_name)
360+
return False
361+
else:
362+
return True
363+
364+
filtered_dbs = list(filter(modified_files_test, compilation_dbs))
365+
return filtered_dbs
366+
367+
262368
def find_error_message(data):
263369
return data[data.rfind(':') + 2:]
264370

0 commit comments

Comments
 (0)