github.com/zhouyu0/docker-note@v0.0.0-20190722021225-b8d3825084db/builder/dockerfile/dispatchers_test.go (about) 1 package dockerfile // import "github.com/docker/docker/builder/dockerfile" 2 3 import ( 4 "bytes" 5 "context" 6 "runtime" 7 "testing" 8 9 "github.com/docker/docker/api/types" 10 "github.com/docker/docker/api/types/backend" 11 "github.com/docker/docker/api/types/container" 12 "github.com/docker/docker/api/types/strslice" 13 "github.com/docker/docker/builder" 14 "github.com/docker/docker/image" 15 "github.com/docker/docker/pkg/system" 16 "github.com/docker/go-connections/nat" 17 "github.com/moby/buildkit/frontend/dockerfile/instructions" 18 "github.com/moby/buildkit/frontend/dockerfile/shell" 19 "gotest.tools/assert" 20 is "gotest.tools/assert/cmp" 21 ) 22 23 func newBuilderWithMockBackend() *Builder { 24 mockBackend := &MockBackend{} 25 opts := &types.ImageBuildOptions{} 26 ctx := context.Background() 27 b := &Builder{ 28 options: opts, 29 docker: mockBackend, 30 Stdout: new(bytes.Buffer), 31 clientCtx: ctx, 32 disableCommit: true, 33 imageSources: newImageSources(ctx, builderOptions{ 34 Options: opts, 35 Backend: mockBackend, 36 }), 37 imageProber: newImageProber(mockBackend, nil, false), 38 containerManager: newContainerManager(mockBackend), 39 } 40 return b 41 } 42 43 func TestEnv2Variables(t *testing.T) { 44 b := newBuilderWithMockBackend() 45 sb := newDispatchRequest(b, '\\', nil, NewBuildArgs(make(map[string]*string)), newStagesBuildResults()) 46 envCommand := &instructions.EnvCommand{ 47 Env: instructions.KeyValuePairs{ 48 instructions.KeyValuePair{Key: "var1", Value: "val1"}, 49 instructions.KeyValuePair{Key: "var2", Value: "val2"}, 50 }, 51 } 52 err := dispatch(sb, envCommand) 53 assert.NilError(t, err) 54 55 expected := []string{ 56 "var1=val1", 57 "var2=val2", 58 } 59 assert.Check(t, is.DeepEqual(expected, sb.state.runConfig.Env)) 60 } 61 62 func TestEnvValueWithExistingRunConfigEnv(t *testing.T) { 63 b := newBuilderWithMockBackend() 64 sb := newDispatchRequest(b, '\\', nil, NewBuildArgs(make(map[string]*string)), newStagesBuildResults()) 65 sb.state.runConfig.Env = []string{"var1=old", "var2=fromenv"} 66 envCommand := &instructions.EnvCommand{ 67 Env: instructions.KeyValuePairs{ 68 instructions.KeyValuePair{Key: "var1", Value: "val1"}, 69 }, 70 } 71 err := dispatch(sb, envCommand) 72 assert.NilError(t, err) 73 expected := []string{ 74 "var1=val1", 75 "var2=fromenv", 76 } 77 assert.Check(t, is.DeepEqual(expected, sb.state.runConfig.Env)) 78 } 79 80 func TestMaintainer(t *testing.T) { 81 maintainerEntry := "Some Maintainer <maintainer@example.com>" 82 b := newBuilderWithMockBackend() 83 sb := newDispatchRequest(b, '\\', nil, NewBuildArgs(make(map[string]*string)), newStagesBuildResults()) 84 cmd := &instructions.MaintainerCommand{Maintainer: maintainerEntry} 85 err := dispatch(sb, cmd) 86 assert.NilError(t, err) 87 assert.Check(t, is.Equal(maintainerEntry, sb.state.maintainer)) 88 } 89 90 func TestLabel(t *testing.T) { 91 labelName := "label" 92 labelValue := "value" 93 94 b := newBuilderWithMockBackend() 95 sb := newDispatchRequest(b, '\\', nil, NewBuildArgs(make(map[string]*string)), newStagesBuildResults()) 96 cmd := &instructions.LabelCommand{ 97 Labels: instructions.KeyValuePairs{ 98 instructions.KeyValuePair{Key: labelName, Value: labelValue}, 99 }, 100 } 101 err := dispatch(sb, cmd) 102 assert.NilError(t, err) 103 104 assert.Assert(t, is.Contains(sb.state.runConfig.Labels, labelName)) 105 assert.Check(t, is.Equal(sb.state.runConfig.Labels[labelName], labelValue)) 106 } 107 108 func TestFromScratch(t *testing.T) { 109 b := newBuilderWithMockBackend() 110 sb := newDispatchRequest(b, '\\', nil, NewBuildArgs(make(map[string]*string)), newStagesBuildResults()) 111 cmd := &instructions.Stage{ 112 BaseName: "scratch", 113 } 114 err := initializeStage(sb, cmd) 115 116 if runtime.GOOS == "windows" && !system.LCOWSupported() { 117 assert.Check(t, is.Error(err, "Linux containers are not supported on this system")) 118 return 119 } 120 121 assert.NilError(t, err) 122 assert.Check(t, sb.state.hasFromImage()) 123 assert.Check(t, is.Equal("", sb.state.imageID)) 124 expected := "PATH=" + system.DefaultPathEnv(runtime.GOOS) 125 assert.Check(t, is.DeepEqual([]string{expected}, sb.state.runConfig.Env)) 126 } 127 128 func TestFromWithArg(t *testing.T) { 129 tag, expected := ":sometag", "expectedthisid" 130 131 getImage := func(name string) (builder.Image, builder.ROLayer, error) { 132 assert.Check(t, is.Equal("alpine"+tag, name)) 133 return &mockImage{id: "expectedthisid"}, nil, nil 134 } 135 b := newBuilderWithMockBackend() 136 b.docker.(*MockBackend).getImageFunc = getImage 137 args := NewBuildArgs(make(map[string]*string)) 138 139 val := "sometag" 140 metaArg := instructions.ArgCommand{KeyValuePairOptional: instructions.KeyValuePairOptional{ 141 Key: "THETAG", 142 Value: &val, 143 }} 144 cmd := &instructions.Stage{ 145 BaseName: "alpine:${THETAG}", 146 } 147 err := processMetaArg(metaArg, shell.NewLex('\\'), args) 148 149 sb := newDispatchRequest(b, '\\', nil, args, newStagesBuildResults()) 150 assert.NilError(t, err) 151 err = initializeStage(sb, cmd) 152 assert.NilError(t, err) 153 154 assert.Check(t, is.Equal(expected, sb.state.imageID)) 155 assert.Check(t, is.Equal(expected, sb.state.baseImage.ImageID())) 156 assert.Check(t, is.Len(sb.state.buildArgs.GetAllAllowed(), 0)) 157 assert.Check(t, is.Len(sb.state.buildArgs.GetAllMeta(), 1)) 158 } 159 160 func TestFromWithArgButBuildArgsNotGiven(t *testing.T) { 161 b := newBuilderWithMockBackend() 162 args := NewBuildArgs(make(map[string]*string)) 163 164 metaArg := instructions.ArgCommand{} 165 cmd := &instructions.Stage{ 166 BaseName: "${THETAG}", 167 } 168 err := processMetaArg(metaArg, shell.NewLex('\\'), args) 169 170 sb := newDispatchRequest(b, '\\', nil, args, newStagesBuildResults()) 171 assert.NilError(t, err) 172 err = initializeStage(sb, cmd) 173 assert.Error(t, err, "base name (${THETAG}) should not be blank") 174 } 175 176 func TestFromWithUndefinedArg(t *testing.T) { 177 tag, expected := "sometag", "expectedthisid" 178 179 getImage := func(name string) (builder.Image, builder.ROLayer, error) { 180 assert.Check(t, is.Equal("alpine", name)) 181 return &mockImage{id: "expectedthisid"}, nil, nil 182 } 183 b := newBuilderWithMockBackend() 184 b.docker.(*MockBackend).getImageFunc = getImage 185 sb := newDispatchRequest(b, '\\', nil, NewBuildArgs(make(map[string]*string)), newStagesBuildResults()) 186 187 b.options.BuildArgs = map[string]*string{"THETAG": &tag} 188 189 cmd := &instructions.Stage{ 190 BaseName: "alpine${THETAG}", 191 } 192 err := initializeStage(sb, cmd) 193 assert.NilError(t, err) 194 assert.Check(t, is.Equal(expected, sb.state.imageID)) 195 } 196 197 func TestFromMultiStageWithNamedStage(t *testing.T) { 198 b := newBuilderWithMockBackend() 199 firstFrom := &instructions.Stage{BaseName: "someimg", Name: "base"} 200 secondFrom := &instructions.Stage{BaseName: "base"} 201 previousResults := newStagesBuildResults() 202 firstSB := newDispatchRequest(b, '\\', nil, NewBuildArgs(make(map[string]*string)), previousResults) 203 secondSB := newDispatchRequest(b, '\\', nil, NewBuildArgs(make(map[string]*string)), previousResults) 204 err := initializeStage(firstSB, firstFrom) 205 assert.NilError(t, err) 206 assert.Check(t, firstSB.state.hasFromImage()) 207 previousResults.indexed["base"] = firstSB.state.runConfig 208 previousResults.flat = append(previousResults.flat, firstSB.state.runConfig) 209 err = initializeStage(secondSB, secondFrom) 210 assert.NilError(t, err) 211 assert.Check(t, secondSB.state.hasFromImage()) 212 } 213 214 func TestOnbuild(t *testing.T) { 215 b := newBuilderWithMockBackend() 216 sb := newDispatchRequest(b, '\\', nil, NewBuildArgs(make(map[string]*string)), newStagesBuildResults()) 217 cmd := &instructions.OnbuildCommand{ 218 Expression: "ADD . /app/src", 219 } 220 err := dispatch(sb, cmd) 221 assert.NilError(t, err) 222 assert.Check(t, is.Equal("ADD . /app/src", sb.state.runConfig.OnBuild[0])) 223 } 224 225 func TestWorkdir(t *testing.T) { 226 b := newBuilderWithMockBackend() 227 sb := newDispatchRequest(b, '`', nil, NewBuildArgs(make(map[string]*string)), newStagesBuildResults()) 228 sb.state.baseImage = &mockImage{} 229 workingDir := "/app" 230 if runtime.GOOS == "windows" { 231 workingDir = "C:\\app" 232 } 233 cmd := &instructions.WorkdirCommand{ 234 Path: workingDir, 235 } 236 237 err := dispatch(sb, cmd) 238 assert.NilError(t, err) 239 assert.Check(t, is.Equal(workingDir, sb.state.runConfig.WorkingDir)) 240 } 241 242 func TestCmd(t *testing.T) { 243 b := newBuilderWithMockBackend() 244 sb := newDispatchRequest(b, '`', nil, NewBuildArgs(make(map[string]*string)), newStagesBuildResults()) 245 sb.state.baseImage = &mockImage{} 246 command := "./executable" 247 248 cmd := &instructions.CmdCommand{ 249 ShellDependantCmdLine: instructions.ShellDependantCmdLine{ 250 CmdLine: strslice.StrSlice{command}, 251 PrependShell: true, 252 }, 253 } 254 err := dispatch(sb, cmd) 255 assert.NilError(t, err) 256 257 var expectedCommand strslice.StrSlice 258 if runtime.GOOS == "windows" { 259 expectedCommand = strslice.StrSlice(append([]string{"cmd"}, "/S", "/C", command)) 260 } else { 261 expectedCommand = strslice.StrSlice(append([]string{"/bin/sh"}, "-c", command)) 262 } 263 264 assert.Check(t, is.DeepEqual(expectedCommand, sb.state.runConfig.Cmd)) 265 assert.Check(t, sb.state.cmdSet) 266 } 267 268 func TestHealthcheckNone(t *testing.T) { 269 b := newBuilderWithMockBackend() 270 sb := newDispatchRequest(b, '`', nil, NewBuildArgs(make(map[string]*string)), newStagesBuildResults()) 271 cmd := &instructions.HealthCheckCommand{ 272 Health: &container.HealthConfig{ 273 Test: []string{"NONE"}, 274 }, 275 } 276 err := dispatch(sb, cmd) 277 assert.NilError(t, err) 278 279 assert.Assert(t, sb.state.runConfig.Healthcheck != nil) 280 assert.Check(t, is.DeepEqual([]string{"NONE"}, sb.state.runConfig.Healthcheck.Test)) 281 } 282 283 func TestHealthcheckCmd(t *testing.T) { 284 285 b := newBuilderWithMockBackend() 286 sb := newDispatchRequest(b, '`', nil, NewBuildArgs(make(map[string]*string)), newStagesBuildResults()) 287 expectedTest := []string{"CMD-SHELL", "curl -f http://localhost/ || exit 1"} 288 cmd := &instructions.HealthCheckCommand{ 289 Health: &container.HealthConfig{ 290 Test: expectedTest, 291 }, 292 } 293 err := dispatch(sb, cmd) 294 assert.NilError(t, err) 295 296 assert.Assert(t, sb.state.runConfig.Healthcheck != nil) 297 assert.Check(t, is.DeepEqual(expectedTest, sb.state.runConfig.Healthcheck.Test)) 298 } 299 300 func TestEntrypoint(t *testing.T) { 301 b := newBuilderWithMockBackend() 302 sb := newDispatchRequest(b, '`', nil, NewBuildArgs(make(map[string]*string)), newStagesBuildResults()) 303 sb.state.baseImage = &mockImage{} 304 entrypointCmd := "/usr/sbin/nginx" 305 306 cmd := &instructions.EntrypointCommand{ 307 ShellDependantCmdLine: instructions.ShellDependantCmdLine{ 308 CmdLine: strslice.StrSlice{entrypointCmd}, 309 PrependShell: true, 310 }, 311 } 312 err := dispatch(sb, cmd) 313 assert.NilError(t, err) 314 assert.Assert(t, sb.state.runConfig.Entrypoint != nil) 315 316 var expectedEntrypoint strslice.StrSlice 317 if runtime.GOOS == "windows" { 318 expectedEntrypoint = strslice.StrSlice(append([]string{"cmd"}, "/S", "/C", entrypointCmd)) 319 } else { 320 expectedEntrypoint = strslice.StrSlice(append([]string{"/bin/sh"}, "-c", entrypointCmd)) 321 } 322 assert.Check(t, is.DeepEqual(expectedEntrypoint, sb.state.runConfig.Entrypoint)) 323 } 324 325 func TestExpose(t *testing.T) { 326 b := newBuilderWithMockBackend() 327 sb := newDispatchRequest(b, '`', nil, NewBuildArgs(make(map[string]*string)), newStagesBuildResults()) 328 329 exposedPort := "80" 330 cmd := &instructions.ExposeCommand{ 331 Ports: []string{exposedPort}, 332 } 333 err := dispatch(sb, cmd) 334 assert.NilError(t, err) 335 336 assert.Assert(t, sb.state.runConfig.ExposedPorts != nil) 337 assert.Assert(t, is.Len(sb.state.runConfig.ExposedPorts, 1)) 338 339 portsMapping, err := nat.ParsePortSpec(exposedPort) 340 assert.NilError(t, err) 341 assert.Check(t, is.Contains(sb.state.runConfig.ExposedPorts, portsMapping[0].Port)) 342 } 343 344 func TestUser(t *testing.T) { 345 b := newBuilderWithMockBackend() 346 sb := newDispatchRequest(b, '`', nil, NewBuildArgs(make(map[string]*string)), newStagesBuildResults()) 347 348 cmd := &instructions.UserCommand{ 349 User: "test", 350 } 351 err := dispatch(sb, cmd) 352 assert.NilError(t, err) 353 assert.Check(t, is.Equal("test", sb.state.runConfig.User)) 354 } 355 356 func TestVolume(t *testing.T) { 357 b := newBuilderWithMockBackend() 358 sb := newDispatchRequest(b, '`', nil, NewBuildArgs(make(map[string]*string)), newStagesBuildResults()) 359 360 exposedVolume := "/foo" 361 362 cmd := &instructions.VolumeCommand{ 363 Volumes: []string{exposedVolume}, 364 } 365 err := dispatch(sb, cmd) 366 assert.NilError(t, err) 367 assert.Assert(t, sb.state.runConfig.Volumes != nil) 368 assert.Check(t, is.Len(sb.state.runConfig.Volumes, 1)) 369 assert.Check(t, is.Contains(sb.state.runConfig.Volumes, exposedVolume)) 370 } 371 372 func TestStopSignal(t *testing.T) { 373 if runtime.GOOS == "windows" { 374 t.Skip("Windows does not support stopsignal") 375 return 376 } 377 b := newBuilderWithMockBackend() 378 sb := newDispatchRequest(b, '`', nil, NewBuildArgs(make(map[string]*string)), newStagesBuildResults()) 379 sb.state.baseImage = &mockImage{} 380 signal := "SIGKILL" 381 382 cmd := &instructions.StopSignalCommand{ 383 Signal: signal, 384 } 385 err := dispatch(sb, cmd) 386 assert.NilError(t, err) 387 assert.Check(t, is.Equal(signal, sb.state.runConfig.StopSignal)) 388 } 389 390 func TestArg(t *testing.T) { 391 b := newBuilderWithMockBackend() 392 sb := newDispatchRequest(b, '`', nil, NewBuildArgs(make(map[string]*string)), newStagesBuildResults()) 393 394 argName := "foo" 395 argVal := "bar" 396 cmd := &instructions.ArgCommand{KeyValuePairOptional: instructions.KeyValuePairOptional{Key: argName, Value: &argVal}} 397 err := dispatch(sb, cmd) 398 assert.NilError(t, err) 399 400 expected := map[string]string{argName: argVal} 401 assert.Check(t, is.DeepEqual(expected, sb.state.buildArgs.GetAllAllowed())) 402 } 403 404 func TestShell(t *testing.T) { 405 b := newBuilderWithMockBackend() 406 sb := newDispatchRequest(b, '`', nil, NewBuildArgs(make(map[string]*string)), newStagesBuildResults()) 407 408 shellCmd := "powershell" 409 cmd := &instructions.ShellCommand{Shell: strslice.StrSlice{shellCmd}} 410 411 err := dispatch(sb, cmd) 412 assert.NilError(t, err) 413 414 expectedShell := strslice.StrSlice([]string{shellCmd}) 415 assert.Check(t, is.DeepEqual(expectedShell, sb.state.runConfig.Shell)) 416 } 417 418 func TestPrependEnvOnCmd(t *testing.T) { 419 buildArgs := NewBuildArgs(nil) 420 buildArgs.AddArg("NO_PROXY", nil) 421 422 args := []string{"sorted=nope", "args=not", "http_proxy=foo", "NO_PROXY=YA"} 423 cmd := []string{"foo", "bar"} 424 cmdWithEnv := prependEnvOnCmd(buildArgs, args, cmd) 425 expected := strslice.StrSlice([]string{ 426 "|3", "NO_PROXY=YA", "args=not", "sorted=nope", "foo", "bar"}) 427 assert.Check(t, is.DeepEqual(expected, cmdWithEnv)) 428 } 429 430 func TestRunWithBuildArgs(t *testing.T) { 431 b := newBuilderWithMockBackend() 432 args := NewBuildArgs(make(map[string]*string)) 433 args.argsFromOptions["HTTP_PROXY"] = strPtr("FOO") 434 b.disableCommit = false 435 sb := newDispatchRequest(b, '`', nil, args, newStagesBuildResults()) 436 437 runConfig := &container.Config{} 438 origCmd := strslice.StrSlice([]string{"cmd", "in", "from", "image"}) 439 cmdWithShell := strslice.StrSlice(append(getShell(runConfig, runtime.GOOS), "echo foo")) 440 envVars := []string{"|1", "one=two"} 441 cachedCmd := strslice.StrSlice(append(envVars, cmdWithShell...)) 442 443 imageCache := &mockImageCache{ 444 getCacheFunc: func(parentID string, cfg *container.Config) (string, error) { 445 // Check the runConfig.Cmd sent to probeCache() 446 assert.Check(t, is.DeepEqual(cachedCmd, cfg.Cmd)) 447 assert.Check(t, is.DeepEqual(strslice.StrSlice(nil), cfg.Entrypoint)) 448 return "", nil 449 }, 450 } 451 452 mockBackend := b.docker.(*MockBackend) 453 mockBackend.makeImageCacheFunc = func(_ []string) builder.ImageCache { 454 return imageCache 455 } 456 b.imageProber = newImageProber(mockBackend, nil, false) 457 mockBackend.getImageFunc = func(_ string) (builder.Image, builder.ROLayer, error) { 458 return &mockImage{ 459 id: "abcdef", 460 config: &container.Config{Cmd: origCmd}, 461 }, nil, nil 462 } 463 mockBackend.containerCreateFunc = func(config types.ContainerCreateConfig) (container.ContainerCreateCreatedBody, error) { 464 // Check the runConfig.Cmd sent to create() 465 assert.Check(t, is.DeepEqual(cmdWithShell, config.Config.Cmd)) 466 assert.Check(t, is.Contains(config.Config.Env, "one=two")) 467 assert.Check(t, is.DeepEqual(strslice.StrSlice{""}, config.Config.Entrypoint)) 468 return container.ContainerCreateCreatedBody{ID: "12345"}, nil 469 } 470 mockBackend.commitFunc = func(cfg backend.CommitConfig) (image.ID, error) { 471 // Check the runConfig.Cmd sent to commit() 472 assert.Check(t, is.DeepEqual(origCmd, cfg.Config.Cmd)) 473 assert.Check(t, is.DeepEqual(cachedCmd, cfg.ContainerConfig.Cmd)) 474 assert.Check(t, is.DeepEqual(strslice.StrSlice(nil), cfg.Config.Entrypoint)) 475 return "", nil 476 } 477 from := &instructions.Stage{BaseName: "abcdef"} 478 err := initializeStage(sb, from) 479 assert.NilError(t, err) 480 sb.state.buildArgs.AddArg("one", strPtr("two")) 481 run := &instructions.RunCommand{ 482 ShellDependantCmdLine: instructions.ShellDependantCmdLine{ 483 CmdLine: strslice.StrSlice{"echo foo"}, 484 PrependShell: true, 485 }, 486 } 487 assert.NilError(t, dispatch(sb, run)) 488 489 // Check that runConfig.Cmd has not been modified by run 490 assert.Check(t, is.DeepEqual(origCmd, sb.state.runConfig.Cmd)) 491 } 492 493 func TestRunIgnoresHealthcheck(t *testing.T) { 494 b := newBuilderWithMockBackend() 495 args := NewBuildArgs(make(map[string]*string)) 496 sb := newDispatchRequest(b, '`', nil, args, newStagesBuildResults()) 497 b.disableCommit = false 498 499 origCmd := strslice.StrSlice([]string{"cmd", "in", "from", "image"}) 500 501 imageCache := &mockImageCache{ 502 getCacheFunc: func(parentID string, cfg *container.Config) (string, error) { 503 return "", nil 504 }, 505 } 506 507 mockBackend := b.docker.(*MockBackend) 508 mockBackend.makeImageCacheFunc = func(_ []string) builder.ImageCache { 509 return imageCache 510 } 511 b.imageProber = newImageProber(mockBackend, nil, false) 512 mockBackend.getImageFunc = func(_ string) (builder.Image, builder.ROLayer, error) { 513 return &mockImage{ 514 id: "abcdef", 515 config: &container.Config{Cmd: origCmd}, 516 }, nil, nil 517 } 518 mockBackend.containerCreateFunc = func(config types.ContainerCreateConfig) (container.ContainerCreateCreatedBody, error) { 519 return container.ContainerCreateCreatedBody{ID: "12345"}, nil 520 } 521 mockBackend.commitFunc = func(cfg backend.CommitConfig) (image.ID, error) { 522 return "", nil 523 } 524 from := &instructions.Stage{BaseName: "abcdef"} 525 err := initializeStage(sb, from) 526 assert.NilError(t, err) 527 528 expectedTest := []string{"CMD-SHELL", "curl -f http://localhost/ || exit 1"} 529 cmd := &instructions.HealthCheckCommand{ 530 Health: &container.HealthConfig{ 531 Test: expectedTest, 532 }, 533 } 534 assert.NilError(t, dispatch(sb, cmd)) 535 assert.Assert(t, sb.state.runConfig.Healthcheck != nil) 536 537 mockBackend.containerCreateFunc = func(config types.ContainerCreateConfig) (container.ContainerCreateCreatedBody, error) { 538 // Check the Healthcheck is disabled. 539 assert.Check(t, is.DeepEqual([]string{"NONE"}, config.Config.Healthcheck.Test)) 540 return container.ContainerCreateCreatedBody{ID: "123456"}, nil 541 } 542 543 sb.state.buildArgs.AddArg("one", strPtr("two")) 544 run := &instructions.RunCommand{ 545 ShellDependantCmdLine: instructions.ShellDependantCmdLine{ 546 CmdLine: strslice.StrSlice{"echo foo"}, 547 PrependShell: true, 548 }, 549 } 550 assert.NilError(t, dispatch(sb, run)) 551 assert.Check(t, is.DeepEqual(expectedTest, sb.state.runConfig.Healthcheck.Test)) 552 }