diff --git a/ament_clang_format/ament_clang_format/main.py b/ament_clang_format/ament_clang_format/main.py
index 87120628..671742f1 100755
--- a/ament_clang_format/ament_clang_format/main.py
+++ b/ament_clang_format/ament_clang_format/main.py
@@ -23,6 +23,7 @@
from xml.sax.saxutils import escape
from xml.sax.saxutils import quoteattr
+from ament_lint.filesystem_helpers import find_executable
import yaml
@@ -245,16 +246,6 @@ def main(argv=sys.argv[1:]):
return rc
-def find_executable(file_names):
- paths = os.getenv('PATH').split(os.path.pathsep)
- for file_name in file_names:
- for path in paths:
- file_path = os.path.join(path, file_name)
- if os.path.isfile(file_path) and os.access(file_path, os.X_OK):
- return file_path
- return None
-
-
def get_files(paths, extensions):
files = []
for path in paths:
diff --git a/ament_clang_format/package.xml b/ament_clang_format/package.xml
index 68f97ef8..37a229f7 100644
--- a/ament_clang_format/package.xml
+++ b/ament_clang_format/package.xml
@@ -17,6 +17,7 @@
Dirk Thomas
Michel Hidalgo
+ ament_lint
clang-format
python3-yaml
diff --git a/ament_clang_format/test/test_execution.py b/ament_clang_format/test/test_execution.py
new file mode 100644
index 00000000..3358460f
--- /dev/null
+++ b/ament_clang_format/test/test_execution.py
@@ -0,0 +1,23 @@
+# Copyright 2025 Open Source Robotics Foundation, Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from ament_clang_format.main import main
+from ament_lint.test_helpers import TempFileWriter
+
+
+def test_clang_format_execution():
+ """Test that clang format can be executed on a simple C++ file, via ament_clang_format."""
+ with TempFileWriter('int main() { return 0; }', 'test.cpp') as temp_file_path:
+ rc = main(argv=['ament_clang_format', temp_file_path])
+ assert rc == 0, 'Clang format found issues'
diff --git a/ament_clang_tidy/ament_clang_tidy/main.py b/ament_clang_tidy/ament_clang_tidy/main.py
index 5faa8142..9d9c978d 100755
--- a/ament_clang_tidy/ament_clang_tidy/main.py
+++ b/ament_clang_tidy/ament_clang_tidy/main.py
@@ -28,6 +28,7 @@
from xml.sax.saxutils import escape
from xml.sax.saxutils import quoteattr
+from ament_lint.filesystem_helpers import find_executable
import yaml
@@ -264,15 +265,7 @@ def start_subprocess(full_cmd):
with open(args.xunit_file, 'w') as f:
f.write(xml)
-
-def find_executable(file_names):
- paths = os.getenv('PATH').split(os.path.pathsep)
- for file_name in file_names:
- for path in paths:
- file_path = os.path.join(path, file_name)
- if os.path.isfile(file_path) and os.access(file_path, os.X_OK):
- return file_path
- return None
+ return 0 if all(len(v) == 0 for v in report.values()) else 1
def get_compilation_db_files(paths):
diff --git a/ament_clang_tidy/package.xml b/ament_clang_tidy/package.xml
index 7db3d8f9..ae160070 100644
--- a/ament_clang_tidy/package.xml
+++ b/ament_clang_tidy/package.xml
@@ -16,6 +16,7 @@
Claire Wang
Michel Hidalgo
+ ament_lint
clang-tidy
python3-yaml
diff --git a/ament_clang_tidy/test/test_execution.py b/ament_clang_tidy/test/test_execution.py
new file mode 100644
index 00000000..62dc4064
--- /dev/null
+++ b/ament_clang_tidy/test/test_execution.py
@@ -0,0 +1,40 @@
+# Copyright 2025 Open Source Robotics Foundation, Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import json
+from pathlib import Path
+
+from ament_clang_tidy.main import main
+from ament_lint.test_helpers import TempFileWriter
+
+
+def test_clang_tidy_execution():
+ """Test that clang tidy can be executed on a simple C++ file, via ament_clang_tidy."""
+ with TempFileWriter('int main() { return 0; }', 'test.cpp') as temp_file_path:
+ temp_dir = Path(temp_file_path).parent
+
+ # Create compile_commands.json
+ compile_commands = [
+ {
+ 'directory': str(temp_dir),
+ 'command': 'c++ -c test.cpp',
+ 'file': str(temp_file_path),
+ }
+ ]
+ compile_commands_json = json.dumps(compile_commands)
+ with TempFileWriter(
+ compile_commands_json, 'compile_commands.json'
+ ) as compile_commands_path:
+ rc = main(argv=['ament_clang_tidy', str(compile_commands_path)])
+ assert rc == 0, 'Clang tidy found issues'
diff --git a/ament_lint/ament_lint/filesystem_helpers.py b/ament_lint/ament_lint/filesystem_helpers.py
new file mode 100644
index 00000000..9bcfc5ab
--- /dev/null
+++ b/ament_lint/ament_lint/filesystem_helpers.py
@@ -0,0 +1,22 @@
+# Copyright 2025 Open Source Robotics Foundation, Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import shutil
+
+def find_executable(file_names: list[str]) -> str | None:
+ for name in file_names:
+ found = shutil.which(name)
+ if found:
+ return found
+ return None
diff --git a/ament_lint/ament_lint/test_helpers.py b/ament_lint/ament_lint/test_helpers.py
new file mode 100644
index 00000000..4ec77f68
--- /dev/null
+++ b/ament_lint/ament_lint/test_helpers.py
@@ -0,0 +1,44 @@
+# Copyright 2025 Open Source Robotics Foundation, Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import tempfile
+import os
+
+class TempFileWriter:
+ """Context manager that writes a string to a temp file and cleans up on exit."""
+
+ def __init__(self, content, filename):
+ self.content = content
+ self.filename = filename
+ self.temp_dir = None
+ self.temp_file = None
+
+ def __enter__(self):
+ # Create a new temporary directory
+ self.temp_dir = tempfile.mkdtemp()
+ # Create the temp file path
+ self.temp_file = os.path.join(self.temp_dir, self.filename)
+ # Write content to the file
+ with open(self.temp_file, 'w') as f:
+ f.write(self.content)
+ return self.temp_file
+
+ def __exit__(self, exc_type, exc_val, exc_tb):
+ # Remove the file
+ if self.temp_file and os.path.exists(self.temp_file):
+ os.remove(self.temp_file)
+ # Remove the directory
+ if self.temp_dir and os.path.exists(self.temp_dir):
+ os.rmdir(self.temp_dir)
+ return False