Skip to content

Commit 34755c8

Browse files
authored
Merge pull request #589 from petercorke/feat/rtbtool-smoke-test
2 parents 13af024 + ae3ba73 commit 34755c8

2 files changed

Lines changed: 92 additions & 11 deletions

File tree

src/roboticstoolbox/bin/rtbtool.py

Lines changed: 70 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,14 @@ def parse_arguments():
150150
action="store_true",
151151
help="use Swift as default backend",
152152
)
153+
parser.add_argument(
154+
"--test",
155+
default=False,
156+
action="store_true",
157+
help="non-interactive environment smoke test: print package versions, "
158+
"exercise one real numeric code path per package, exit 0/1 "
159+
"instead of starting an interactive shell",
160+
)
153161

154162
argv = env_arguments(parser) + sys.argv[1:]
155163
args, rest = parser.parse_known_args(argv)
@@ -160,6 +168,18 @@ def parse_arguments():
160168
return args, rest
161169

162170

171+
def get_versions() -> list[str]:
172+
"""Package version strings shown in the banner and by --test."""
173+
return [
174+
f"RTB=={version('roboticstoolbox-python')}",
175+
f"SMTB=={version('spatialmath-python')}",
176+
f"SG=={version('spatialgeometry')}",
177+
f"NumPy=={version('numpy')}",
178+
f"SciPy=={version('scipy')}",
179+
f"Matplotlib=={version('matplotlib')}",
180+
]
181+
182+
163183
def make_banner():
164184
# banner template
165185
# https://patorjk.com/software/taag/#p=display&f=Cybermedium&t=Robotics%20Toolbox%0A
@@ -171,16 +191,8 @@ def make_banner():
171191
172192
for Python"""
173193

174-
versions = []
175-
versions.append(f"RTB=={version('roboticstoolbox-python')}")
176-
versions.append(f"SMTB=={version('spatialmath-python')}")
177-
versions.append(f"SG=={version('spatialmath-python')}")
178-
versions.append(f"NumPy=={version('numpy')}")
179-
versions.append(f"SciPy=={version('scipy')}")
180-
versions.append(f"Matplotlib=={version('matplotlib')}")
181-
182194
# create banner
183-
banner += " (" + ", ".join(versions) + ")"
195+
banner += " (" + ", ".join(get_versions()) + ")"
184196
banner += r"""
185197
186198
import math
@@ -215,7 +227,56 @@ def startup():
215227
plt.ion()
216228

217229

230+
def run_smoke_test() -> bool:
231+
"""Non-interactive environment sanity check, used by --test.
232+
233+
Not a substitute for the pytest suite -- a fast, human- or script-run
234+
"did this environment actually come together correctly" check: real
235+
versions, confirmation the compiled extensions loaded, and one real
236+
numeric result compared against a known-correct value. The last part
237+
matters specifically because a compiled extension built against the
238+
wrong NumPy ABI can load successfully and still compute garbage --
239+
checking that it merely *imported* wouldn't catch that.
240+
"""
241+
print(", ".join(get_versions()))
242+
243+
from roboticstoolbox.ets.fknm import _C_AVAILABLE as fknm_c
244+
from roboticstoolbox.robot.frne import _C_AVAILABLE as frne_c
245+
246+
panda = models.DH.Panda()
247+
T = panda.fkine(panda.qr).A
248+
expected = np.array(
249+
[
250+
[9.9500416528e-01, 0.0000000000e00, 9.9833416647e-02, 4.8400688203e-01],
251+
[0.0000000000e00, -1.0000000000e00, -1.2032944640e-16, -6.8775459668e-17],
252+
[9.9833416647e-02, 1.2490009027e-16, -9.9500416528e-01, 4.1302777713e-01],
253+
[0.0000000000e00, 0.0000000000e00, 0.0000000000e00, 1.0000000000e00],
254+
]
255+
)
256+
257+
checks = [
258+
("fknm compiled extension loaded", fknm_c),
259+
("frne compiled extension loaded", frne_c),
260+
(
261+
"Panda.fkine(qr) matches expected (1e-9)",
262+
bool(np.allclose(T, expected, atol=1e-9)),
263+
),
264+
]
265+
266+
for name, passed in checks:
267+
print(f"[{'PASS' if passed else 'FAIL'}] {name}")
268+
269+
n_passed = sum(1 for _, passed in checks if passed)
270+
print(f"rtbtool --test: {n_passed}/{len(checks)} checks passed")
271+
return n_passed == len(checks)
272+
273+
218274
def main():
275+
args, ipython_args = parse_arguments()
276+
277+
if args.test:
278+
sys.exit(0 if run_smoke_test() else 1)
279+
219280
try:
220281
import IPython
221282
from IPython.terminal.prompts import Prompts
@@ -228,8 +289,6 @@ def main():
228289
" pip install roboticstoolbox-python[tool]\n"
229290
)
230291

231-
args, ipython_args = parse_arguments()
232-
233292
# setup defaults
234293
np.set_printoptions(
235294
linewidth=120,

tests/test_bin.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,28 @@ def test_run_script(self):
5454
self.assertEqual(result.returncode, 0, msg=result.stderr.decode())
5555
self.assertIn(b"SENTINEL_OUTPUT Panda", result.stdout)
5656

57+
def test_smoke_test_flag(self):
58+
"""--test should run non-interactively and report all checks passing."""
59+
result = _run(["roboticstoolbox.bin.rtbtool", "--test"])
60+
self.assertEqual(result.returncode, 0, msg=result.stderr.decode())
61+
out = result.stdout.decode()
62+
self.assertIn("[PASS] fknm compiled extension loaded", out)
63+
self.assertIn("[PASS] frne compiled extension loaded", out)
64+
self.assertIn("[PASS] Panda.fkine(qr) matches expected", out)
65+
self.assertIn("rtbtool --test: 3/3 checks passed", out)
66+
67+
def test_smoke_test_reports_distinct_package_versions(self):
68+
# Regression test: the banner/--test version line once printed
69+
# spatialmath-python's version twice (once labelled SG) instead of
70+
# spatialgeometry's own -- catch any recurrence by requiring the
71+
# two version numbers to actually be looked up independently.
72+
from importlib.metadata import version
73+
74+
result = _run(["roboticstoolbox.bin.rtbtool", "--test"])
75+
out = result.stdout.decode()
76+
self.assertIn(f"SMTB=={version('spatialmath-python')}", out)
77+
self.assertIn(f"SG=={version('spatialgeometry')}", out)
78+
5779
def test_options_envvar(self):
5880
"""RTB_OPTIONS should be parsed the same as command-line arguments."""
5981
env = dict(os.environ, RTB_OPTIONS="--prompt envtest>")

0 commit comments

Comments
 (0)