66 "sync"
77 "testing"
88
9+ "go.uber.org/mock/gomock"
10+
911 "github.com/devcontainers/cli/internal/imagemeta"
1012 "github.com/devcontainers/cli/internal/log"
1113)
@@ -29,15 +31,6 @@ func (l *lockedLog) has(s string) bool {
2931 return slices .Contains (l .lines , s )
3032}
3133
32- // orderExecutor records every command, and every AfterPostCreate call as
33- // "<dotfiles>", in the order they happen.
34- type orderExecutor struct { seen []string }
35-
36- func (o * orderExecutor ) Exec (command string ) error {
37- o .seen = append (o .seen , command )
38- return nil
39- }
40-
4134// skipPostAttach skips postAttachCommand alone, and the dotfiles hook runs once,
4235// right after postCreateCommand (TS runLifecycleHooks / installDotfiles).
4336func TestRunHooksSkipsOnlyPostAttachAndInstallsDotfilesAfterPostCreate (t * testing.T ) {
@@ -48,14 +41,27 @@ func TestRunHooksSkipsOnlyPostAttachAndInstallsDotfilesAfterPostCreate(t *testin
4841 PostStartCommands : []any {"postStart" },
4942 PostAttachCommands : []any {"postAttach" },
5043 }
51- exec := & orderExecutor {}
52- opts := RunOptions {SkipPostAttach : true , AfterPostCreate : func () { exec .seen = append (exec .seen , "<dotfiles>" ) }}
44+ dotfiles := 0
45+ dotfilesRanBefore := func (want int ) func (string ) {
46+ return func (command string ) {
47+ if dotfiles != want {
48+ t .Errorf ("%s ran after %d dotfiles installs, want %d" , command , dotfiles , want )
49+ }
50+ }
51+ }
52+ exec := NewMockCommandExecutor (gomock .NewController (t ))
53+ gomock .InOrder (
54+ exec .EXPECT ().Exec ("onCreate" ),
55+ exec .EXPECT ().Exec ("updateContent" ),
56+ exec .EXPECT ().Exec ("postCreate" ).Do (dotfilesRanBefore (0 )),
57+ exec .EXPECT ().Exec ("postStart" ).Do (dotfilesRanBefore (1 )),
58+ )
59+ opts := RunOptions {SkipPostAttach : true , AfterPostCreate : func () { dotfiles ++ }}
5360 if err := RunHooks (log .Null , exec , merged , opts ); err != nil {
5461 t .Fatal (err )
5562 }
56- want := []string {"onCreate" , "updateContent" , "postCreate" , "<dotfiles>" , "postStart" }
57- if ! slices .Equal (exec .seen , want ) {
58- t .Errorf ("ran %v, want %v" , exec .seen , want )
63+ if dotfiles != 1 {
64+ t .Errorf ("dotfiles installed %d times, want once" , dotfiles )
5965 }
6066}
6167
@@ -66,7 +72,10 @@ func TestRunHooksSkipsOnlyPostAttachAndInstallsDotfilesAfterPostCreate(t *testin
6672func TestParallelCommandHeaderOnlyForSuccess (t * testing.T ) {
6773 l := & lockedLog {Logger : log .Null }
6874 cmd := map [string ]any {"a" : "ok-a" , "b" : "fails-b" }
69- if err := executeCommand (& cmdErrExecutor {failOn : "fails" }, l , PhasePostCreate , cmd , "" ); err == nil {
75+ exec := NewMockCommandExecutor (gomock .NewController (t ))
76+ exec .EXPECT ().Exec ("ok-a" )
77+ exec .EXPECT ().Exec ("fails-b" ).Return (exitedOne ("fails-b" ))
78+ if err := executeCommand (exec , l , PhasePostCreate , cmd , "" ); err == nil {
7079 t .Fatal ("expected b's failure" )
7180 }
7281 if ! l .has ("Running a of postCreateCommand from devcontainer.json..." ) {
@@ -80,24 +89,22 @@ func TestParallelCommandHeaderOnlyForSuccess(t *testing.T) {
8089 }
8190}
8291
83- // codeExecutor fails every command with the given exit code.
84- type codeExecutor struct { code int }
85-
86- func (c codeExecutor ) Exec (command string ) error {
87- return & CommandError {Command : command , ExitCode : c .code }
88- }
89-
9092// TS runLifecycleCommand writes "failed with exit code N" only when the error
9193// carries a code (`if (err?.code)`): exit code 1 is reported, 0 is not.
9294func TestFailureLineNeedsANonZeroExitCode (t * testing.T ) {
9395 const line = "postStartCommand from devcontainer.json failed with exit code 1. Skipping any further user-provided commands."
96+ exitsWith := func (code int ) CommandExecutor {
97+ exec := NewMockCommandExecutor (gomock .NewController (t ))
98+ exec .EXPECT ().Exec ("x" ).Return (& CommandError {Command : "x" , ExitCode : code })
99+ return exec
100+ }
94101 l := & lockedLog {Logger : log .Null }
95- _ = executeCommand (codeExecutor { code : 1 } , l , PhasePostStart , "x" , "" )
102+ _ = executeCommand (exitsWith ( 1 ) , l , PhasePostStart , "x" , "" )
96103 if ! l .has (line ) {
97104 t .Errorf ("exit code 1: lines = %q, want %q" , l .lines , line )
98105 }
99106 l = & lockedLog {Logger : log .Null }
100- _ = executeCommand (codeExecutor { code : 0 } , l , PhasePostStart , "x" , "" )
107+ _ = executeCommand (exitsWith ( 0 ) , l , PhasePostStart , "x" , "" )
101108 for _ , got := range l .lines {
102109 if strings .Contains (got , "failed with exit code" ) {
103110 t .Errorf ("exit code 0 reported a failure line: %q" , got )
0 commit comments