|
24 | 24 | from launch.actions import DeclareLaunchArgument |
25 | 25 | from launch.actions import IncludeLaunchDescription |
26 | 26 | from launch.actions import OpaqueFunction |
| 27 | +from launch.actions import PopEnvironment |
| 28 | +from launch.actions import PopLaunchConfigurations |
| 29 | +from launch.actions import PushEnvironment |
| 30 | +from launch.actions import PushLaunchConfigurations |
27 | 31 | from launch.actions import ResetLaunchConfigurations |
28 | 32 | from launch.actions import SetEnvironmentVariable |
29 | 33 | from launch.actions import SetLaunchConfiguration |
|
34 | 38 |
|
35 | 39 | import pytest |
36 | 40 |
|
| 41 | +from temporary_environment import sandbox_environment_variables |
| 42 | + |
37 | 43 |
|
38 | 44 | def test_include_launch_description_constructors(): |
39 | 45 | """Test the constructors for IncludeLaunchDescription class.""" |
@@ -259,6 +265,176 @@ def test_include_launch_description_launch_arguments(): |
259 | 265 | action2.visit(lc2) |
260 | 266 |
|
261 | 267 |
|
| 268 | +@sandbox_environment_variables |
| 269 | +def test_include_launch_description_scoped_execute(): |
| 270 | + """Test scoped=True: Push/Pop wrapping, forwarding, and isolation of launch configurations.""" |
| 271 | + ld_child = LaunchDescription([]) |
| 272 | + action = IncludeLaunchDescription( |
| 273 | + LaunchDescriptionSource(ld_child), |
| 274 | + launch_arguments={'bar': 'BAR'}.items(), |
| 275 | + scoped=True, |
| 276 | + ) |
| 277 | + |
| 278 | + lc = LaunchContext() |
| 279 | + lc.launch_configurations['foo'] = 'FOO' |
| 280 | + |
| 281 | + result = action.visit(lc) |
| 282 | + |
| 283 | + # Expected: Push, Push, SetLaunchConfig, LaunchDescription, OpaqueFunction, Pop, Pop |
| 284 | + assert len(result) == 7 |
| 285 | + assert isinstance(result[0], PushLaunchConfigurations) |
| 286 | + assert isinstance(result[1], PushEnvironment) |
| 287 | + assert isinstance(result[2], SetLaunchConfiguration) |
| 288 | + assert result[3] == ld_child |
| 289 | + assert isinstance(result[4], OpaqueFunction) |
| 290 | + assert isinstance(result[5], PopEnvironment) |
| 291 | + assert isinstance(result[6], PopLaunchConfigurations) |
| 292 | + |
| 293 | + # Step through and verify intermediate state |
| 294 | + result[0].visit(lc) # PushLaunchConfigurations |
| 295 | + assert lc.launch_configurations['foo'] == 'FOO' # forwarded to child scope |
| 296 | + |
| 297 | + result[1].visit(lc) # PushEnvironment |
| 298 | + |
| 299 | + result[2].visit(lc) # SetLaunchConfiguration('bar', 'BAR') |
| 300 | + assert lc.launch_configurations['bar'] == 'BAR' |
| 301 | + assert lc.launch_configurations['foo'] == 'FOO' # still visible |
| 302 | + |
| 303 | + # Simulate what the child launch description would do |
| 304 | + lc.launch_configurations['baz'] = 'BAZ' |
| 305 | + assert lc.launch_configurations['baz'] == 'BAZ' |
| 306 | + |
| 307 | + # result[3] (LaunchDescription) and result[4] (OpaqueFunction) skipped — they don't affect |
| 308 | + # launch_configurations directly in this test |
| 309 | + |
| 310 | + result[5].visit(lc) # PopEnvironment |
| 311 | + result[6].visit(lc) # PopLaunchConfigurations |
| 312 | + # After pop, child's configs are gone, parent's are restored |
| 313 | + assert lc.launch_configurations['foo'] == 'FOO' |
| 314 | + assert 'baz' not in lc.launch_configurations |
| 315 | + assert 'bar' not in lc.launch_configurations |
| 316 | + assert len(lc.launch_configurations) == 1 |
| 317 | + |
| 318 | + |
| 319 | +@sandbox_environment_variables |
| 320 | +def test_include_launch_description_unscoped_execute(): |
| 321 | + """Test scoped=False (default): no Push/Pop, configurations leak to parent.""" |
| 322 | + ld_child = LaunchDescription([]) |
| 323 | + action = IncludeLaunchDescription( |
| 324 | + LaunchDescriptionSource(ld_child), |
| 325 | + launch_arguments={'bar': 'BAR'}.items(), |
| 326 | + ) |
| 327 | + |
| 328 | + lc = LaunchContext() |
| 329 | + lc.launch_configurations['foo'] = 'FOO' |
| 330 | + |
| 331 | + result = action.visit(lc) |
| 332 | + |
| 333 | + # Expected: SetLaunchConfig, LaunchDescription, OpaqueFunction (no Push/Pop) |
| 334 | + assert len(result) == 3 |
| 335 | + assert isinstance(result[0], SetLaunchConfiguration) |
| 336 | + assert result[1] == ld_child |
| 337 | + assert isinstance(result[2], OpaqueFunction) |
| 338 | + assert not any(isinstance(r, PushLaunchConfigurations) for r in result) |
| 339 | + assert not any(isinstance(r, PopLaunchConfigurations) for r in result) |
| 340 | + |
| 341 | + # Step through |
| 342 | + result[0].visit(lc) # SetLaunchConfiguration('bar', 'BAR') |
| 343 | + assert lc.launch_configurations['bar'] == 'BAR' |
| 344 | + assert lc.launch_configurations['foo'] == 'FOO' # untouched |
| 345 | + |
| 346 | + # After all actions, bar persists — it leaked to the parent scope |
| 347 | + assert len(lc.launch_configurations) == 2 |
| 348 | + assert lc.launch_configurations['bar'] == 'BAR' |
| 349 | + |
| 350 | + |
| 351 | +@sandbox_environment_variables |
| 352 | +def test_include_launch_description_scoped_isolates_environment(): |
| 353 | + """Test scoped=True: environment variable changes do not leak to parent.""" |
| 354 | + ld_child = LaunchDescription([]) |
| 355 | + action = IncludeLaunchDescription( |
| 356 | + LaunchDescriptionSource(ld_child), |
| 357 | + scoped=True, |
| 358 | + ) |
| 359 | + |
| 360 | + lc = LaunchContext() |
| 361 | + assert 'env_foo' not in lc.environment |
| 362 | + |
| 363 | + result = action.visit(lc) |
| 364 | + |
| 365 | + assert isinstance(result[0], PushLaunchConfigurations) |
| 366 | + assert isinstance(result[1], PushEnvironment) |
| 367 | + |
| 368 | + result[0].visit(lc) # PushLaunchConfigurations |
| 369 | + result[1].visit(lc) # PushEnvironment |
| 370 | + |
| 371 | + # Simulate child setting an environment variable |
| 372 | + lc.environment['env_foo'] = 'FOO' |
| 373 | + assert lc.environment['env_foo'] == 'FOO' |
| 374 | + |
| 375 | + assert isinstance(result[-2], PopEnvironment) |
| 376 | + assert isinstance(result[-1], PopLaunchConfigurations) |
| 377 | + |
| 378 | + result[-2].visit(lc) # PopEnvironment |
| 379 | + assert 'env_foo' not in lc.environment # rolled back |
| 380 | + |
| 381 | + result[-1].visit(lc) # PopLaunchConfigurations |
| 382 | + |
| 383 | + |
| 384 | +@sandbox_environment_variables |
| 385 | +def test_include_launch_description_unscoped_leaks_environment(): |
| 386 | + """Test scoped=False (default): environment variable changes leak to parent.""" |
| 387 | + ld_child = LaunchDescription([]) |
| 388 | + action = IncludeLaunchDescription( |
| 389 | + LaunchDescriptionSource(ld_child), |
| 390 | + ) |
| 391 | + |
| 392 | + lc = LaunchContext() |
| 393 | + result = action.visit(lc) |
| 394 | + |
| 395 | + # No Push/Pop — environment mutations persist |
| 396 | + assert len(result) == 2 # LaunchDescription, OpaqueFunction (no launch_arguments) |
| 397 | + |
| 398 | + # Simulate child setting an environment variable |
| 399 | + lc.environment['env_foo'] = 'FOO' |
| 400 | + |
| 401 | + # After all actions, the env var persists — no Pop to roll it back |
| 402 | + assert lc.environment['env_foo'] == 'FOO' |
| 403 | + |
| 404 | + |
| 405 | +@sandbox_environment_variables |
| 406 | +def test_include_launch_description_scoped_with_overwrite(): |
| 407 | + """Test scoped=True: child overwrites parent config, but parent value is restored after pop.""" |
| 408 | + ld_child = LaunchDescription([]) |
| 409 | + action = IncludeLaunchDescription( |
| 410 | + LaunchDescriptionSource(ld_child), |
| 411 | + launch_arguments={'foo': 'OOF'}.items(), |
| 412 | + scoped=True, |
| 413 | + ) |
| 414 | + |
| 415 | + lc = LaunchContext() |
| 416 | + lc.launch_configurations['foo'] = 'FOO' |
| 417 | + lc.launch_configurations['bar'] = 'BAR' |
| 418 | + |
| 419 | + result = action.visit(lc) |
| 420 | + |
| 421 | + result[0].visit(lc) # PushLaunchConfigurations |
| 422 | + assert lc.launch_configurations['foo'] == 'FOO' # copied to new scope |
| 423 | + assert lc.launch_configurations['bar'] == 'BAR' # forwarded |
| 424 | + |
| 425 | + result[1].visit(lc) # PushEnvironment |
| 426 | + |
| 427 | + result[2].visit(lc) # SetLaunchConfiguration('foo', 'OOF') |
| 428 | + assert lc.launch_configurations['foo'] == 'OOF' # overwritten in child scope |
| 429 | + assert lc.launch_configurations['bar'] == 'BAR' # untouched |
| 430 | + |
| 431 | + result[-2].visit(lc) # PopEnvironment |
| 432 | + result[-1].visit(lc) # PopLaunchConfigurations |
| 433 | + assert lc.launch_configurations['foo'] == 'FOO' # restored |
| 434 | + assert lc.launch_configurations['bar'] == 'BAR' # still there |
| 435 | + assert len(lc.launch_configurations) == 2 |
| 436 | + |
| 437 | + |
262 | 438 | def test_include_python(): |
263 | 439 | """Test including Python, with and without explicit PythonLaunchDescriptionSource.""" |
264 | 440 | this_dir = Path(__file__).parent |
|
0 commit comments