Skip to content

Commit ff5852c

Browse files
committed
implemented workspace tests
1 parent d1ea0a3 commit ff5852c

4 files changed

Lines changed: 235 additions & 43 deletions

File tree

package_xml_validation/helpers/find_launch_dependencies.py

Lines changed: 5 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@
88

99
import os
1010
import re
11-
import argparse
1211

1312

1413
REGEX_EXPR = [
@@ -52,6 +51,11 @@ def scan_file(path, found: set[str], verbose: bool = False):
5251

5352

5453
def scan_files(launch_dir: str, verbose: bool = False) -> list[str]:
54+
"""
55+
Extracts launch dependencies from the specified directory.
56+
Launch dependencies are listed packages names in the launch files.
57+
It uses regex to extract package names from common launch patterns.
58+
"""
5559
if not os.path.isdir(launch_dir):
5660
print(f"Error: '{launch_dir}' is not a directory.")
5761
return []
@@ -63,30 +67,3 @@ def scan_files(launch_dir: str, verbose: bool = False) -> list[str]:
6367
if fn.endswith((".py", ".xml", ".yaml", ".launch", ".yml")):
6468
scan_file(os.path.join(root, fn), pkgs, verbose)
6569
return list(pkgs)
66-
67-
68-
def parse_args():
69-
p = argparse.ArgumentParser(
70-
description="Extract referenced ROS 2 package names from launch files via regex."
71-
)
72-
p.add_argument("launch_dir", help="Path to your package's launch/ directory")
73-
p.add_argument(
74-
"-v", "--verbose", action="store_true", help="Print found packages to stdout"
75-
)
76-
# parse args
77-
args = p.parse_args()
78-
pkgs = scan_files(args.launch_dir)
79-
if args.verbose:
80-
print("Found packages:")
81-
for pkg in sorted(pkgs):
82-
print(f" - {pkg}")
83-
84-
85-
if __name__ == "__main__":
86-
# parse_args()
87-
file = "/home/aljoscha-schmidt/hector/src/simulation_scenario_robocup_gazebo/launch/launch_world.launch.py"
88-
found = set()
89-
scan_file(file, found, verbose=True)
90-
print("Found packages:")
91-
for pkg in sorted(found):
92-
print(f" - {pkg}")

package_xml_validation/helpers/pkg_xml_formatter.py

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -608,11 +608,11 @@ def add_member_of_group(self, root, group_name: str):
608608
root.insert(insert_position, member_of_group)
609609

610610

611-
if __name__ == "__main__":
612-
# Example usage
613-
pkg = "/home/aljoscha-schmidt/hector/src/hector_gamepad_manager/hector_gamepad_manager/package.xml"
614-
formatter = PackageXmlFormatter(
615-
check_only=False,
616-
verbose=True,
617-
check_with_xmllint=True,
618-
)
611+
# if __name__ == "__main__":
612+
# # Example usage
613+
# pkg = "/home/aljoscha-schmidt/hector/src/hector_gamepad_manager/hector_gamepad_manager/package.xml"
614+
# formatter = PackageXmlFormatter(
615+
# check_only=False,
616+
# verbose=True,
617+
# check_with_xmllint=True,
618+
# )

package_xml_validation/helpers/rosdep_validator.py

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -60,10 +60,10 @@ def check_rosdeps_and_local_pkgs(self, dependencies) -> list[str]:
6060
return unresolvable
6161

6262

63-
if __name__ == "__main__":
64-
dependencies = ["rclcpp", "nonexistent_dependency", "hector_gamepad_manager"]
65-
validator = RosdepValidator()
66-
# Example usage
67-
unresolvable = validator.check_rosdeps(dependencies)
68-
for dep in unresolvable:
69-
print(f"Could not resolve dependency: {dep}")
63+
# if __name__ == "__main__":
64+
# dependencies = ["rclcpp", "nonexistent_dependency", "hector_gamepad_manager"]
65+
# validator = RosdepValidator()
66+
# # Example usage
67+
# unresolvable = validator.check_rosdeps(dependencies)
68+
# for dep in unresolvable:
69+
# print(f"Could not resolve dependency: {dep}")

tests/test_workspace.py

Lines changed: 215 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,215 @@
1+
# tests/test_workspace.py
2+
import io
3+
import sys
4+
import unittest
5+
import tempfile
6+
from pathlib import Path
7+
from contextlib import redirect_stdout
8+
from unittest import mock
9+
10+
from package_xml_validation.helpers import workspace as SUT
11+
12+
13+
class TempTree:
14+
def __init__(self):
15+
self._tmp = tempfile.TemporaryDirectory()
16+
self.root = Path(self._tmp.name)
17+
18+
def ws(self, name="ws"):
19+
ws = self.root / name
20+
(ws / "src").mkdir(parents=True, exist_ok=True)
21+
return ws
22+
23+
def pkg(self, base: Path, pkg_name: str, *, name_in_xml=None, malformed=False):
24+
d = base / pkg_name
25+
d.mkdir(parents=True, exist_ok=True)
26+
xml = d / "package.xml"
27+
if malformed:
28+
xml.write_text("<package><name>oops") # invalid XML
29+
else:
30+
if name_in_xml is None:
31+
xml.write_text("<package></package>")
32+
else:
33+
xml.write_text(f"<package><name>{name_in_xml}</name></package>")
34+
return d
35+
36+
def touch(self, p: Path, content: str = ""):
37+
p.parent.mkdir(parents=True, exist_ok=True)
38+
p.write_text(content)
39+
return p
40+
41+
def close(self):
42+
self._tmp.cleanup()
43+
44+
45+
class TestWorkspaceHelpers(unittest.TestCase):
46+
def setUp(self):
47+
self.t = TempTree()
48+
49+
# Workspace with packages
50+
self.ws = self.t.ws("ws_ok")
51+
src = self.ws / "src"
52+
self.pkg1 = self.t.pkg(src, "pkg1", name_in_xml="pkg1")
53+
self.pkg_nameless = self.t.pkg(src, "pkg_nameless", name_in_xml=None)
54+
55+
self.nested_parent = src / "nested"
56+
self.inner_pkg = self.t.pkg(
57+
self.nested_parent, "inner_pkg", name_in_xml="inner_pkg"
58+
)
59+
60+
# Ignored via COLCON_IGNORE in package dir
61+
self.pkg_ignored = self.t.pkg(src, "pkg_ignored", name_in_xml="pkg_ignored")
62+
(self.pkg_ignored / "COLCON_IGNORE").write_text("")
63+
64+
# Ignored via parent directory COLCON_IGNORE
65+
third_party = src / "third_party"
66+
third_party.mkdir(parents=True, exist_ok=True) # <-- ensure parent exists
67+
(third_party / "COLCON_IGNORE").write_text("")
68+
self.pkg_third = self.t.pkg(third_party, "third_pkg", name_in_xml="third_pkg")
69+
70+
# File inside pkg1
71+
self.t.touch(self.pkg1 / "node.py", "#!/usr/bin/env python3")
72+
73+
# Non-workspace flat folder; used to test fallback in get_pkgs_in_wrs
74+
self.flat = self.t.root / "flat_pkgs"
75+
self.flat.mkdir(parents=True, exist_ok=True)
76+
self.flat_a = self.t.pkg(self.flat, "flat_a", name_in_xml="flat_a")
77+
self.flat_b = self.t.pkg(self.flat, "flat_b", name_in_xml="flat_b")
78+
self.t.touch(self.flat_a / "script.py", "#!/usr/bin/env python3")
79+
80+
def tearDown(self):
81+
self.t.close()
82+
83+
# --- parse_pkg_name -----------------------------------------------------
84+
def test_parse_pkg_name_valid(self):
85+
self.assertEqual(SUT.parse_pkg_name(self.pkg1 / "package.xml"), "pkg1")
86+
87+
def test_parse_pkg_name_missing_name_fallback(self):
88+
self.assertEqual(
89+
SUT.parse_pkg_name(self.pkg_nameless / "package.xml"), "pkg_nameless"
90+
)
91+
92+
def test_parse_pkg_name_malformed_xml_fallback(self):
93+
badroot = self.t.root / "bad"
94+
badpkg = self.t.pkg(badroot, "badpkg", malformed=True)
95+
self.assertEqual(SUT.parse_pkg_name(badpkg / "package.xml"), "badpkg")
96+
97+
# --- find_package_dir ---------------------------------------------------
98+
def test_find_package_dir_from_pkg_dir(self):
99+
self.assertEqual(SUT.find_package_dir(self.pkg1), self.pkg1)
100+
101+
def test_find_package_dir_from_file_path(self):
102+
self.assertEqual(SUT.find_package_dir(self.pkg1 / "node.py"), self.pkg1)
103+
104+
def test_find_package_dir_downward_search_ignores_build_install(self):
105+
ws2 = self.t.ws("ws2")
106+
_ = self.t.pkg(
107+
ws2 / "build", "fake_pkg", name_in_xml="fake_pkg"
108+
) # should be ignored
109+
with self.assertRaises(ValueError):
110+
SUT.find_package_dir(ws2)
111+
112+
def test_find_package_dir_no_package_raises(self):
113+
nowhere = self.t.root / "nowhere"
114+
nowhere.mkdir(parents=True, exist_ok=True)
115+
with self.assertRaises(ValueError):
116+
SUT.find_package_dir(nowhere)
117+
118+
# --- looks_like_ws_root -------------------------------------------------
119+
def test_looks_like_ws_root_true(self):
120+
self.assertTrue(SUT.looks_like_ws_root(self.ws, self.pkg1))
121+
122+
def test_looks_like_ws_root_false(self):
123+
not_ws = self.t.root / "not_ws"
124+
not_ws.mkdir(parents=True, exist_ok=True)
125+
self.assertFalse(SUT.looks_like_ws_root(not_ws, self.pkg1))
126+
127+
# --- find_workspace_root ------------------------------------------------
128+
def test_find_workspace_root_from_pkg_dir(self):
129+
self.assertEqual(SUT.find_workspace_root(self.pkg1), self.ws)
130+
131+
def test_find_workspace_root_from_file(self):
132+
self.assertEqual(SUT.find_workspace_root(self.pkg1 / "node.py"), self.ws)
133+
134+
def test_find_workspace_root_raises_outside_ws(self):
135+
with self.assertRaises(ValueError):
136+
SUT.find_workspace_root(self.flat_a)
137+
138+
# --- pkg_iterator -------------------------------------------------------
139+
def test_pkg_iterator_lists_pkgs_and_respects_colcon_ignore(self):
140+
pkgs = SUT.pkg_iterator(self.ws / "src")
141+
self.assertEqual(set(pkgs.keys()), {"inner_pkg", "pkg1", "pkg_nameless"})
142+
self.assertNotIn("pkg_ignored", pkgs)
143+
self.assertNotIn("third_pkg", pkgs)
144+
145+
# --- get_pkgs_in_wrs ----------------------------------------------------
146+
def test_get_pkgs_in_wrs_with_string_path(self):
147+
names = SUT.get_pkgs_in_wrs(str(self.pkg1 / "node.py"))
148+
self.assertEqual(names, ["inner_pkg", "pkg1", "pkg_nameless"])
149+
150+
def test_get_pkgs_in_wrs_with_path_obj(self):
151+
names = SUT.get_pkgs_in_wrs(self.pkg1 / "node.py")
152+
self.assertEqual(names, ["inner_pkg", "pkg1", "pkg_nameless"])
153+
154+
def test_get_pkgs_in_wrs_fallback_flat_folder(self):
155+
names = SUT.get_pkgs_in_wrs(self.flat_a / "script.py")
156+
self.assertEqual(names, ["flat_a", "flat_b"])
157+
158+
def test_get_pkgs_in_wrs_nonexistent_path_obj_raises_valueerror(self):
159+
bogus = self.t.root / "does" / "not" / "exist"
160+
with self.assertRaises(ValueError):
161+
SUT.get_pkgs_in_wrs(bogus)
162+
163+
def test_get_pkgs_in_wrs_nonexistent_string_raises_filenotfound(self):
164+
bogus = str(self.t.root / "also" / "not" / "here")
165+
with self.assertRaises(FileNotFoundError):
166+
SUT.get_pkgs_in_wrs(bogus)
167+
168+
# --- CLI main() ---------------------------------------------------------
169+
def test_main_lists_packages_names_only(self):
170+
argv = ["prog", str(self.pkg1)]
171+
with mock.patch.object(sys, "argv", argv):
172+
buf = io.StringIO()
173+
with redirect_stdout(buf):
174+
SUT.main()
175+
out = buf.getvalue().strip().splitlines()
176+
self.assertTrue(out[0].startswith("Workspace: "))
177+
listed = set(out[1:])
178+
self.assertTrue({"pkg1", "pkg_nameless", "inner_pkg"}.issubset(listed))
179+
self.assertNotIn("pkg_ignored", listed)
180+
self.assertFalse(any(s.startswith("third_pkg") for s in listed))
181+
182+
def test_main_lists_packages_with_full_paths(self):
183+
argv = ["prog", "--full-paths", str(self.pkg1)]
184+
with mock.patch.object(sys, "argv", argv):
185+
buf = io.StringIO()
186+
with redirect_stdout(buf):
187+
SUT.main()
188+
lines = buf.getvalue().strip().splitlines()[1:]
189+
self.assertTrue(
190+
any(line.startswith("pkg1 ") and str(self.pkg1) in line for line in lines)
191+
)
192+
self.assertTrue(
193+
any(
194+
line.startswith("inner_pkg ") and str(self.inner_pkg) in line
195+
for line in lines
196+
)
197+
)
198+
self.assertTrue(
199+
any(
200+
line.startswith("pkg_nameless ") and str(self.pkg_nameless) in line
201+
for line in lines
202+
)
203+
)
204+
205+
def test_main_exits_when_no_packages_found(self):
206+
ws2 = self.t.ws("ws2_empty")
207+
argv = ["prog", str(ws2)]
208+
with mock.patch.object(sys, "argv", argv), self.assertRaises(SystemExit):
209+
buf = io.StringIO()
210+
with redirect_stdout(buf):
211+
SUT.main()
212+
213+
214+
if __name__ == "__main__":
215+
unittest.main()

0 commit comments

Comments
 (0)