@@ -12,16 +12,74 @@ import (
1212 "os/exec"
1313 "runtime"
1414 "strconv"
15+ "sync/atomic"
1516 "syscall"
1617 "testing"
18+ "time"
1719
1820 "github.com/spf13/afero"
1921
22+ "github.com/DataDog/datadog-agent/cmd/serverless-init/lifecycle"
2023 serverlessLog "github.com/DataDog/datadog-agent/cmd/serverless-init/log"
2124
2225 "github.com/stretchr/testify/assert"
2326)
2427
28+ // TestDetectMode_Sidecar_HasRunnerSet verifies that sidecar mode (no args)
29+ // sets Runner to RunSidecar so main.go can call it directly.
30+ func TestDetectMode_Sidecar_HasRunnerSet (t * testing.T ) {
31+ saved := os .Args
32+ defer func () { os .Args = saved }()
33+ os .Args = []string {"datadog-init" }
34+
35+ conf := DetectMode ()
36+ assert .True (t , conf .SidecarMode )
37+ assert .NotNil (t , conf .Runner , "sidecar mode must set Runner to RunSidecar" )
38+ }
39+
40+ // TestDetectMode_Init_RunnerIsNil verifies that init mode (args present) leaves
41+ // Runner nil. main.go delegates to cloudService.Run(modeConf, logConfig) which
42+ // each CloudService implements directly, so modeConf.Runner is not used in
43+ // init-container mode.
44+ func TestDetectMode_Init_RunnerIsNil (t * testing.T ) {
45+ saved := os .Args
46+ defer func () { os .Args = saved }()
47+ os .Args = []string {"datadog-init" , "sh" , "-c" , "exit 0" }
48+
49+ conf := DetectMode ()
50+ assert .False (t , conf .SidecarMode )
51+ assert .Nil (t , conf .Runner , "init mode Runner must be nil; main.go builds the closure with child" )
52+ }
53+
54+ // TestHandleTerminationSignals_SIGTERM and _SIGINT exercise handleTerminationSignals
55+ // via its injectable notify parameter, avoiding real OS signals and verifying
56+ // that either signal unblocks stopCh.
57+ func TestHandleTerminationSignals_SIGTERM (t * testing.T ) {
58+ stopCh := make (chan struct {}, 1 )
59+ notify := func (c chan <- os.Signal , _ ... os.Signal ) {
60+ go func () { c <- syscall .SIGTERM }()
61+ }
62+ handleTerminationSignals (stopCh , notify )
63+ select {
64+ case <- stopCh :
65+ default :
66+ t .Fatal ("stopCh must be closed after SIGTERM" )
67+ }
68+ }
69+
70+ func TestHandleTerminationSignals_SIGINT (t * testing.T ) {
71+ stopCh := make (chan struct {}, 1 )
72+ notify := func (c chan <- os.Signal , _ ... os.Signal ) {
73+ go func () { c <- syscall .SIGINT }()
74+ }
75+ handleTerminationSignals (stopCh , notify )
76+ select {
77+ case <- stopCh :
78+ default :
79+ t .Fatal ("stopCh must be closed after SIGINT" )
80+ }
81+ }
82+
2583func TestBuildCommandParamWithArgs (t * testing.T ) {
2684 name , args := buildCommandParam ([]string {"superCmd" , "--verbose" , "path" , "-i" , "." })
2785 assert .Equal (t , "superCmd" , name )
@@ -36,19 +94,110 @@ func TestBuildCommandParam(t *testing.T) {
3694
3795func TestPropagateChildSuccess (t * testing.T ) {
3896 runTestOnLinuxOnly (t , func (t * testing.T ) {
39- err := execute (& serverlessLog.Config {}, []string {"bash" , "-c" , "exit 0" })
97+ err := execute (& serverlessLog.Config {}, []string {"bash" , "-c" , "exit 0" }, nil )
4098 assert .Equal (t , nil , err )
4199 })
42100}
43101
44102func TestPropagateChildError (t * testing.T ) {
45103 runTestOnLinuxOnly (t , func (t * testing.T ) {
46104 expectedError := 123
47- err := execute (& serverlessLog.Config {}, []string {"bash" , "-c" , "exit " + strconv .Itoa (expectedError )})
105+ err := execute (& serverlessLog.Config {}, []string {"bash" , "-c" , "exit " + strconv .Itoa (expectedError )}, nil )
48106 assert .Equal (t , expectedError << 8 , int (err .(* exec.ExitError ).ProcessState .Sys ().(syscall.WaitStatus )))
49107 })
50108}
51109
110+ // When cmd.Start fails (e.g. binary not found), execute must return the error
111+ // and never invoke OnAlive — the user app never ran.
112+ func TestExecute_StartFailure_NeverCallsOnAlive (t * testing.T ) {
113+ var onAliveCalled bool
114+ hooks := & ProcessHooks {
115+ OnAlive : func () { onAliveCalled = true },
116+ OnDead : func () {},
117+ }
118+ err := execute (& serverlessLog.Config {}, []string {"/nonexistent/binary/that/cannot/be/found" }, hooks )
119+ assert .Error (t , err , "cmd.Start must fail for a missing binary" )
120+ assert .False (t , onAliveCalled , "OnAlive must not be called when cmd.Start fails" )
121+ }
122+
123+ // On a successful run, execute must call OnAlive after cmd.Start and OnDead
124+ // via defer after cmd.Wait. The mid-run probe pins the ordering.
125+ func TestExecute_SuccessfulRun_InvokesHooksInOrder (t * testing.T ) {
126+ child := lifecycle .NewChild ()
127+ hooks := & ProcessHooks {
128+ OnAlive : child .MarkAlive ,
129+ OnDead : child .MarkDead ,
130+ }
131+ var midRunAlive atomic.Bool
132+ probeDone := make (chan struct {})
133+ go func () {
134+ defer close (probeDone )
135+ time .Sleep (100 * time .Millisecond )
136+ midRunAlive .Store (child .IsAlive ())
137+ }()
138+ err := execute (& serverlessLog.Config {}, []string {"sh" , "-c" , "sleep 0.5" }, hooks )
139+ <- probeDone
140+ assert .NoError (t , err )
141+ assert .True (t , midRunAlive .Load (), "OnAlive must fire before cmd.Wait returns" )
142+ assert .False (t , child .IsAlive (), "OnDead must fire after cmd.Wait returns" )
143+ }
144+
145+ // MicroVM init-container mode: ProcessHooks drive liveness tracking through
146+ // the public RunInit entry point. Pins the alive→dead transition.
147+ func TestRunInit_MicroVM_ChildSupplied_TracksLiveness (t * testing.T ) {
148+ saved := os .Args
149+ defer func () { os .Args = saved }()
150+ os .Args = []string {"datadog-init" , "sh" , "-c" , "sleep 0.5" }
151+
152+ child := lifecycle .NewChild ()
153+ var midRunAlive atomic.Bool
154+ probeDone := make (chan struct {})
155+ go func () {
156+ defer close (probeDone )
157+ time .Sleep (100 * time .Millisecond )
158+ midRunAlive .Store (child .IsAlive ())
159+ }()
160+ err := RunInit (& serverlessLog.Config {}, & ProcessHooks {OnAlive : child .MarkAlive , OnDead : child .MarkDead })
161+ <- probeDone
162+ assert .NoError (t , err )
163+ assert .True (t , midRunAlive .Load (), "child must be marked alive while RunInit is blocked" )
164+ assert .False (t , child .IsAlive (), "child must be marked dead after RunInit returns" )
165+ }
166+
167+ // Non-MicroVM mode: nil hooks. RunInit must execute the user app without
168+ // panicking. Pins the `if hooks != nil` guard.
169+ func TestRunInit_NonMicroVM_NoHooks_StillExecutes (t * testing.T ) {
170+ saved := os .Args
171+ defer func () { os .Args = saved }()
172+ os .Args = []string {"datadog-init" , "sh" , "-c" , "exit 0" }
173+
174+ err := RunInit (& serverlessLog.Config {}, nil )
175+ assert .NoError (t , err )
176+ }
177+
178+ // On a non-zero exit, OnDead must still fire. Pins the defer-on-error path.
179+ func TestExecute_NonZeroExit_OnDeadFiresAfterExit (t * testing.T ) {
180+ child := lifecycle .NewChild ()
181+ // Simulate MicroVM wiring: OnAlive fires first so child is alive, then
182+ // OnDead fires on defer.
183+ hooks := & ProcessHooks {OnAlive : child .MarkAlive , OnDead : child .MarkDead }
184+ err := execute (& serverlessLog.Config {}, []string {"sh" , "-c" , "exit 7" }, hooks )
185+ assert .Error (t , err , "non-zero exit must surface as an error" )
186+ assert .False (t , child .IsAlive (), "OnDead must fire after non-zero exit" )
187+ }
188+
189+ // ProcessHooks with OnAlive set but OnDead nil must not panic. Pins the
190+ // nil-guard on hooks.OnDead added to execute().
191+ func TestExecute_OnAliveSetOnDeadNil_DoesNotPanic (t * testing.T ) {
192+ hooks := & ProcessHooks {
193+ OnAlive : func () {},
194+ OnDead : nil , // intentionally absent
195+ }
196+ assert .NotPanics (t , func () {
197+ _ = execute (& serverlessLog.Config {}, []string {"sh" , "-c" , "exit 0" }, hooks )
198+ })
199+ }
200+
52201func TestForwardSignalToChild (t * testing.T ) {
53202 runTestOnLinuxOnly (t , func (t * testing.T ) {
54203 resultChan := make (chan error )
0 commit comments