@@ -408,4 +408,193 @@ class AnsibleRunnerSpec extends Specification{
408408 ! runner. getTempSshVarsFile(). exists()
409409 ! runner. getTempBecameVarsFile(). exists()
410410 }
411+ def " adhoc: uses -i, no -t, and sets callback envs" () {
412+ given :
413+ def runnerBuilder = AnsibleRunner . adHoc(" ansible.builtin.ping" , null )
414+ .inventory(" /tmp/rdk-inv.ini" )
415+ .limits(" target1" )
416+ .customTmpDirPath(" /tmp" )
417+
418+ def process = Mock (Process ) {
419+ waitFor() >> 0
420+ getInputStream() >> new ByteArrayInputStream (new byte [0 ])
421+ getOutputStream() >> new ByteArrayOutputStream ()
422+ getErrorStream() >> new ByteArrayInputStream (new byte [0 ])
423+ destroy() >> { }
424+ }
425+ def processExecutor = Mock (ProcessExecutor ) {
426+ run() >> process
427+ }
428+
429+ List capturedArgs = null
430+ Map<String ,String > capturedEnv = null
431+
432+ ProcessExecutor.ProcessExecutorBuilder processBuilder =
433+ Mock (ProcessExecutor.ProcessExecutorBuilder )
434+
435+ // keep fluent chain by always returning the builder
436+ processBuilder. build() >> processExecutor
437+ processBuilder. procArgs(_ as List ) >> { List a -> capturedArgs = new ArrayList (a); return processBuilder }
438+ processBuilder. environmentVariables(_ as Map<String ,String > ) >> { Map<String ,String > e -> capturedEnv = new HashMap<> (e); return processBuilder }
439+ processBuilder. baseDirectory(_ as File ) >> { File f -> return processBuilder }
440+ processBuilder. stdinVariables(_ as List ) >> { List v -> return processBuilder }
441+ processBuilder. promptStdinLogFile(_ as File ) >> { File f -> return processBuilder }
442+ processBuilder. debug(_ as boolean ) >> { boolean d -> return processBuilder }
443+
444+ runnerBuilder. processExecutorBuilder(processBuilder)
445+
446+ when :
447+ def rc = runnerBuilder. build(). run()
448+
449+ then :
450+ rc == 0
451+
452+ // --- inventory & deprecation checks (flattened, robust) ---
453+ def invPath = " /tmp/rdk-inv.ini"
454+ assert capturedArgs != null : " proc args were null"
455+
456+ // Flatten in case the mock handed us a nested list (e.g., [[...]])
457+ def flatArgs = capturedArgs. flatten(). collect { it?. toString() }
458+ println " Captured procArgs (flat): ${ flatArgs} "
459+
460+ // preferred form: -i <path>
461+ int iPos = flatArgs. indexOf(" -i" )
462+ boolean hasDashISeparate = (iPos >= 0 && iPos + 1 < flatArgs. size() && flatArgs[iPos + 1 ] == invPath)
463+
464+ // defensive: -i=<path> or -i<path>
465+ boolean hasDashIEquals = flatArgs. any { it == " -i=${ invPath} " || it == " -i${ invPath} " }
466+
467+ // must not use deprecated long flag (covers "--inventory-file" and "--inventory-file=/path")
468+ boolean hasDeprecatedLong =
469+ flatArgs. contains(" --inventory-file" ) ||
470+ flatArgs. any { it. startsWith(" --inventory-file=" ) }
471+
472+ assert (hasDashISeparate || hasDashIEquals) :
473+ " Expected -i ${ invPath} (or -i=${ invPath} ) in args, but got: ${ flatArgs} "
474+ assert ! hasDeprecatedLong : " Found deprecated --inventory-file in args: ${ flatArgs} "
475+
476+ // also ensure no '-t'
477+ assert ! flatArgs. contains(" -t" )
478+
479+ // env for ad-hoc tree replacement
480+ assert capturedEnv != null : " env was null"
481+ capturedEnv. get(" ANSIBLE_LOAD_CALLBACK_PLUGINS" ) == " 1"
482+ capturedEnv. get(" ANSIBLE_CALLBACKS_ENABLED" ) == " ansible.builtin.tree"
483+ capturedEnv. get(" ANSIBLE_CALLBACK_TREE_DIR" ) != null
484+ }
485+
486+ def " playbook path: uses -i but does NOT set ad-hoc callback envs" () {
487+ given :
488+ def runnerBuilder = AnsibleRunner . playbookPath(" /tmp/playbook.yml" )
489+ .inventory(" /tmp/rdk-inv.ini" )
490+ .customTmpDirPath(" /tmp" )
491+
492+ def process = Mock (Process ) {
493+ waitFor() >> 0
494+ getInputStream() >> new ByteArrayInputStream (new byte [0 ])
495+ getOutputStream() >> new ByteArrayOutputStream ()
496+ getErrorStream() >> new ByteArrayInputStream (new byte [0 ])
497+ destroy() >> { }
498+ }
499+ def processExecutor = Mock (ProcessExecutor ) {
500+ run() >> process
501+ }
502+
503+ List capturedArgs = null
504+ Map<String ,String > capturedEnv = null
505+
506+ def processBuilder = Mock (ProcessExecutor.ProcessExecutorBuilder )
507+ processBuilder. build() >> processExecutor
508+ processBuilder. procArgs(_ as List ) >> { List a -> capturedArgs = new ArrayList (a); return processBuilder }
509+ processBuilder. environmentVariables(_ as Map<String ,String > ) >> { Map<String ,String > e -> capturedEnv = new HashMap<> (e); return processBuilder }
510+ processBuilder. baseDirectory(_ as File ) >> { File f -> return processBuilder }
511+ processBuilder. stdinVariables(_ as List ) >> { List v -> return processBuilder }
512+ processBuilder. promptStdinLogFile(_ as File ) >> { File f -> return processBuilder }
513+ processBuilder. debug(_ as boolean ) >> { boolean d -> return processBuilder }
514+
515+ runnerBuilder. processExecutorBuilder(processBuilder)
516+
517+ when :
518+ def rc = runnerBuilder. build(). run()
519+
520+ then :
521+ rc == 0
522+ assert capturedArgs != null : " proc args were null"
523+
524+ // Flatten & stringify to avoid type surprises (and nested lists)
525+ def flatArgs = capturedArgs. flatten(). collect { it?. toString() }
526+ println " Captured procArgs (flat): ${ flatArgs} "
527+
528+ // inventory via -i (or -i=)
529+ def invPath = " /tmp/rdk-inv.ini"
530+ int iPos = flatArgs. indexOf(" -i" )
531+ boolean hasDashISeparate = (iPos >= 0 && iPos + 1 < flatArgs. size() && flatArgs[iPos + 1 ] == invPath)
532+ boolean hasDashIEquals = flatArgs. any { it == " -i=${ invPath} " || it == " -i${ invPath} " }
533+
534+ // must not use deprecated long flag
535+ boolean hasDeprecatedLong =
536+ flatArgs. contains(" --inventory-file" ) ||
537+ flatArgs. any { it. startsWith(" --inventory-file=" ) }
538+
539+ assert (hasDashISeparate || hasDashIEquals) :
540+ " Expected -i ${ invPath} (or -i=${ invPath} ) in args, but got: ${ flatArgs} "
541+ assert ! hasDeprecatedLong : " Found deprecated --inventory-file in args: ${ flatArgs} "
542+
543+ // playbook path should NOT set the ad-hoc callback envs
544+ assert capturedEnv != null : " env was null"
545+ capturedEnv. get(" ANSIBLE_LOAD_CALLBACK_PLUGINS" ) == null
546+ capturedEnv. get(" ANSIBLE_CALLBACKS_ENABLED" ) == null
547+ capturedEnv. get(" ANSIBLE_CALLBACK_TREE_DIR" ) == null
548+ }
549+
550+
551+ def "adhoc: respects user-provided callback envs (putIfAbsent )" () {
552+ given:
553+ def runnerBuilder = AnsibleRunner.adHoc(" ansible. builtin. ping" , null)
554+ .inventory(" / tmp/ rdk- inv. ini" )
555+ .limits(" target1" )
556+ .options([
557+ " ANSIBLE_CALLBACKS_ENABLED " : " custom. callback" ,
558+ " ANSIBLE_LOAD_CALLBACK_PLUGINS " : " 0 " ,
559+ " ANSIBLE_CALLBACK_TREE_DIR " : " / tmp/ custom- tree"
560+ ])
561+ .customTmpDirPath(" / tmp" )
562+
563+ def process = Mock(Process) {
564+ waitFor() >> 0
565+ getInputStream() >> new ByteArrayInputStream(new byte[0])
566+ getOutputStream() >> new ByteArrayOutputStream()
567+ getErrorStream() >> new ByteArrayInputStream(new byte[0])
568+ destroy() >> { }
569+ }
570+ def processExecutor = Mock(ProcessExecutor) {
571+ run() >> process
572+ }
573+
574+ Map<String,String> capturedEnv = null
575+
576+ def processBuilder = Mock(ProcessExecutor.ProcessExecutorBuilder)
577+ processBuilder.build() >> processExecutor
578+ processBuilder.procArgs(_ as List<String>) >> { List<String> a -> return processBuilder }
579+ processBuilder.environmentVariables(_ as Map<String,String>) >> { Map<String,String> e -> capturedEnv = new HashMap<>(e); return processBuilder }
580+ processBuilder.baseDirectory(_ as File) >> { File f -> return processBuilder }
581+ processBuilder.stdinVariables(_ as List) >> { List v -> return processBuilder }
582+ processBuilder.promptStdinLogFile(_ as File) >> { File f -> return processBuilder }
583+ processBuilder.debug(_ as boolean) >> { boolean d -> return processBuilder }
584+
585+ runnerBuilder.processExecutorBuilder(processBuilder)
586+
587+ when:
588+ def rc = runnerBuilder.build().run()
589+
590+ then:
591+ rc == 0
592+ assert capturedEnv != null : " env was null "
593+ // user-provided values win because putIfAbsent was used
594+ capturedEnv.get(" ANSIBLE_CALLBACKS_ENABLED " ) == " custom. callback"
595+ capturedEnv.get(" ANSIBLE_LOAD_CALLBACK_PLUGINS " ) == " 0 "
596+ capturedEnv.get(" ANSIBLE_CALLBACK_TREE_DIR " ) == " / tmp/ custom- tree"
597+ }
598+
599+
411600}
0 commit comments