@@ -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+
163183def 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+
218274def 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 ,
0 commit comments