github.com/containerd/nerdctl@v1.7.7/pkg/testutil/testutil.go (about) 1 /* 2 Copyright The containerd Authors. 3 4 Licensed under the Apache License, Version 2.0 (the "License"); 5 you may not use this file except in compliance with the License. 6 You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10 Unless required by applicable law or agreed to in writing, software 11 distributed under the License is distributed on an "AS IS" BASIS, 12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 See the License for the specific language governing permissions and 14 limitations under the License. 15 */ 16 17 package testutil 18 19 import ( 20 "encoding/json" 21 "flag" 22 "fmt" 23 "io" 24 "os" 25 "os/exec" 26 "path/filepath" 27 "runtime" 28 "strings" 29 "testing" 30 "time" 31 32 "github.com/Masterminds/semver/v3" 33 "github.com/containerd/containerd/defaults" 34 "github.com/containerd/log" 35 "github.com/containerd/nerdctl/pkg/buildkitutil" 36 "github.com/containerd/nerdctl/pkg/imgutil" 37 "github.com/containerd/nerdctl/pkg/infoutil" 38 "github.com/containerd/nerdctl/pkg/inspecttypes/dockercompat" 39 "github.com/containerd/nerdctl/pkg/inspecttypes/native" 40 "github.com/containerd/nerdctl/pkg/platformutil" 41 "github.com/containerd/nerdctl/pkg/rootlessutil" 42 "github.com/opencontainers/go-digest" 43 "gotest.tools/v3/assert" 44 "gotest.tools/v3/icmd" 45 ) 46 47 type Base struct { 48 T testing.TB 49 Target Target 50 DaemonIsKillable bool 51 EnableIPv6 bool 52 IPv6Compatible bool 53 Binary string 54 Args []string 55 Env []string 56 } 57 58 // WithStdin sets the standard input of Cmd to the specified reader 59 func WithStdin(r io.Reader) func(*Cmd) { 60 return func(i *Cmd) { 61 i.Cmd.Stdin = r 62 } 63 } 64 65 func (b *Base) Cmd(args ...string) *Cmd { 66 icmdCmd := icmd.Command(b.Binary, append(b.Args, args...)...) 67 icmdCmd.Env = b.Env 68 cmd := &Cmd{ 69 Cmd: icmdCmd, 70 Base: b, 71 } 72 return cmd 73 } 74 75 // ComposeCmd executes `nerdctl -n nerdctl-test compose` or `docker-compose` 76 func (b *Base) ComposeCmd(args ...string) *Cmd { 77 binary := b.Binary 78 binaryArgs := append(b.Args, append([]string{"compose"}, args...)...) 79 icmdCmd := icmd.Command(binary, binaryArgs...) 80 icmdCmd.Env = b.Env 81 cmd := &Cmd{ 82 Cmd: icmdCmd, 83 Base: b, 84 } 85 return cmd 86 } 87 88 func (b *Base) ComposeCmdWithHelper(helper []string, args ...string) *Cmd { 89 helperBin, err := exec.LookPath(helper[0]) 90 if err != nil { 91 b.T.Skipf("helper binary %q not found", helper[0]) 92 } 93 binary := b.Binary 94 binaryArgs := append(b.Args, append([]string{"compose"}, args...)...) 95 96 helperArgs := helper[1:] 97 helperArgs = append(helperArgs, binary) 98 helperArgs = append(helperArgs, binaryArgs...) 99 icmdCmd := icmd.Command(helperBin, helperArgs...) 100 icmdCmd.Env = b.Env 101 cmd := &Cmd{ 102 Cmd: icmdCmd, 103 Base: b, 104 } 105 return cmd 106 } 107 108 func (b *Base) CmdWithHelper(helper []string, args ...string) *Cmd { 109 helperBin, err := exec.LookPath(helper[0]) 110 if err != nil { 111 b.T.Skipf("helper binary %q not found", helper[0]) 112 } 113 helperArgs := helper[1:] 114 helperArgs = append(helperArgs, b.Binary) 115 helperArgs = append(helperArgs, b.Args...) 116 helperArgs = append(helperArgs, args...) 117 118 icmdCmd := icmd.Command(helperBin, helperArgs...) 119 cmd := &Cmd{ 120 Cmd: icmdCmd, 121 Base: b, 122 } 123 return cmd 124 } 125 126 func (b *Base) systemctlTarget() string { 127 switch b.Target { 128 case Nerdctl: 129 return "containerd.service" 130 case Docker: 131 return "docker.service" 132 default: 133 b.T.Fatalf("unexpected target %q", b.Target) 134 return "" 135 } 136 } 137 138 func (b *Base) systemctlArgs() []string { 139 var systemctlArgs []string 140 if os.Geteuid() != 0 { 141 systemctlArgs = append(systemctlArgs, "--user") 142 } 143 return systemctlArgs 144 } 145 146 func (b *Base) KillDaemon() { 147 b.T.Helper() 148 if !b.DaemonIsKillable { 149 b.T.Skip("daemon is not killable (hint: set \"-test.kill-daemon\")") 150 } 151 target := b.systemctlTarget() 152 b.T.Logf("killing %q", target) 153 cmdKill := exec.Command("systemctl", 154 append(b.systemctlArgs(), 155 []string{"kill", "-s", "KILL", target}...)...) 156 if out, err := cmdKill.CombinedOutput(); err != nil { 157 err = fmt.Errorf("cannot kill %q: %q: %w", target, string(out), err) 158 b.T.Fatal(err) 159 } 160 // the daemon should restart automatically 161 } 162 163 func (b *Base) EnsureDaemonActive() { 164 b.T.Helper() 165 target := b.systemctlTarget() 166 b.T.Logf("checking activity of %q", target) 167 systemctlArgs := b.systemctlArgs() 168 const ( 169 maxRetry = 30 170 sleep = 3 * time.Second 171 ) 172 for i := 0; i < maxRetry; i++ { 173 cmd := exec.Command("systemctl", 174 append(systemctlArgs, 175 []string{"is-active", target}...)...) 176 out, err := cmd.CombinedOutput() 177 b.T.Logf("(retry=%d) %s", i, string(out)) 178 if err == nil { 179 // The daemon is now running, but the daemon may still refuse connections to containerd.sock 180 b.T.Logf("daemon %q is now running, checking whether the daemon can handle requests", target) 181 infoRes := b.Cmd("info").Run() 182 if infoRes.ExitCode == 0 { 183 b.T.Logf("daemon %q can now handle requests", target) 184 return 185 } 186 b.T.Logf("(retry=%d) %s", i, infoRes.Combined()) 187 } 188 time.Sleep(sleep) 189 } 190 b.T.Fatalf("daemon %q not running?", target) 191 } 192 193 func (b *Base) DumpDaemonLogs(minutes int) { 194 b.T.Helper() 195 target := b.systemctlTarget() 196 cmd := exec.Command("journalctl", 197 append(b.systemctlArgs(), 198 []string{"-u", target, 199 "--no-pager", 200 "-S", fmt.Sprintf("%d min ago", minutes)}...)...) 201 b.T.Logf("===== %v =====", cmd.Args) 202 out, err := cmd.CombinedOutput() 203 if err != nil { 204 b.T.Fatal(err) 205 } 206 b.T.Log(string(out)) 207 b.T.Log("==========") 208 } 209 210 func (b *Base) InspectContainer(name string) dockercompat.Container { 211 cmdResult := b.Cmd("container", "inspect", name).Run() 212 assert.Equal(b.T, cmdResult.ExitCode, 0) 213 var dc []dockercompat.Container 214 if err := json.Unmarshal([]byte(cmdResult.Stdout()), &dc); err != nil { 215 b.T.Fatal(err) 216 } 217 assert.Equal(b.T, 1, len(dc)) 218 return dc[0] 219 } 220 221 func (b *Base) InspectImage(name string) dockercompat.Image { 222 cmdResult := b.Cmd("image", "inspect", name).Run() 223 assert.Equal(b.T, cmdResult.ExitCode, 0) 224 var dc []dockercompat.Image 225 if err := json.Unmarshal([]byte(cmdResult.Stdout()), &dc); err != nil { 226 b.T.Fatal(err) 227 } 228 assert.Equal(b.T, 1, len(dc)) 229 return dc[0] 230 } 231 232 func (b *Base) InspectNetwork(name string) dockercompat.Network { 233 cmdResult := b.Cmd("network", "inspect", name).Run() 234 assert.Equal(b.T, cmdResult.ExitCode, 0) 235 var dc []dockercompat.Network 236 if err := json.Unmarshal([]byte(cmdResult.Stdout()), &dc); err != nil { 237 b.T.Fatal(err) 238 } 239 assert.Equal(b.T, 1, len(dc)) 240 return dc[0] 241 } 242 243 func (b *Base) InspectVolume(name string, args ...string) native.Volume { 244 cmd := append([]string{"volume", "inspect"}, args...) 245 cmd = append(cmd, name) 246 cmdResult := b.Cmd(cmd...).Run() 247 assert.Equal(b.T, cmdResult.ExitCode, 0) 248 var dc []native.Volume 249 if err := json.Unmarshal([]byte(cmdResult.Stdout()), &dc); err != nil { 250 b.T.Fatal(err) 251 } 252 assert.Equal(b.T, 1, len(dc)) 253 return dc[0] 254 } 255 256 func (b *Base) Info() dockercompat.Info { 257 cmdResult := b.Cmd("info", "--format", "{{ json . }}").Run() 258 assert.Equal(b.T, cmdResult.ExitCode, 0) 259 var info dockercompat.Info 260 if err := json.Unmarshal([]byte(cmdResult.Stdout()), &info); err != nil { 261 b.T.Fatal(err) 262 } 263 return info 264 } 265 266 func (b *Base) InfoNative() native.Info { 267 b.T.Helper() 268 if GetTarget() != Nerdctl { 269 b.T.Skip("InfoNative() should not be called for non-nerdctl target") 270 } 271 cmdResult := b.Cmd("info", "--mode", "native", "--format", "{{ json . }}").Run() 272 assert.Equal(b.T, cmdResult.ExitCode, 0) 273 var info native.Info 274 if err := json.Unmarshal([]byte(cmdResult.Stdout()), &info); err != nil { 275 b.T.Fatal(err) 276 } 277 return info 278 } 279 280 func (b *Base) ContainerdAddress() string { 281 b.T.Helper() 282 if GetTarget() != Nerdctl { 283 b.T.Skip("ContainerdAddress() should not be called for non-nerdctl target") 284 } 285 if os.Geteuid() == 0 { 286 return defaults.DefaultAddress 287 } 288 xdr, err := rootlessutil.XDGRuntimeDir() 289 if err != nil { 290 b.T.Log(err) 291 xdr = fmt.Sprintf("/run/user/%d", os.Geteuid()) 292 } 293 pidFile := filepath.Join(xdr, "containerd-rootless", "child_pid") 294 pidB, err := os.ReadFile(pidFile) 295 if err != nil { 296 b.T.Fatal(err) 297 } 298 pidS := strings.TrimSpace(string(pidB)) 299 return filepath.Join("/proc", pidS, "root", defaults.DefaultAddress) 300 } 301 302 func (b *Base) EnsureContainerStarted(con string) { 303 b.T.Helper() 304 305 const ( 306 maxRetry = 5 307 sleep = time.Second 308 ) 309 for i := 0; i < maxRetry; i++ { 310 if b.InspectContainer(con).State.Running { 311 b.T.Logf("container %s is now running", con) 312 return 313 } 314 b.T.Logf("(retry=%d)", i+1) 315 time.Sleep(sleep) 316 } 317 b.T.Fatalf("conainer %s not running", con) 318 } 319 320 func (b *Base) EnsureContainerExited(con string, expectedExitCode int) { 321 b.T.Helper() 322 323 const ( 324 maxRetry = 5 325 sleep = time.Second 326 ) 327 var c dockercompat.Container 328 for i := 0; i < maxRetry; i++ { 329 c = b.InspectContainer(con) 330 if c.State.Status == "exited" { 331 b.T.Logf("container %s have exited with status %d", con, c.State.ExitCode) 332 if c.State.ExitCode == expectedExitCode { 333 return 334 } 335 break 336 } 337 b.T.Logf("(retry=%d)", i+1) 338 time.Sleep(sleep) 339 } 340 b.T.Fatalf("expected conainer %s to have exited with code %d, got status %+v", 341 con, expectedExitCode, c.State) 342 } 343 344 type Cmd struct { 345 icmd.Cmd 346 *Base 347 } 348 349 func (c *Cmd) Run() *icmd.Result { 350 c.Base.T.Helper() 351 return icmd.RunCmd(c.Cmd) 352 } 353 354 func (c *Cmd) Start() *icmd.Result { 355 c.Base.T.Helper() 356 return icmd.StartCmd(c.Cmd) 357 } 358 359 func (c *Cmd) CmdOption(cmdOptions ...func(*Cmd)) *Cmd { 360 for _, opt := range cmdOptions { 361 opt(c) 362 } 363 return c 364 } 365 366 func (c *Cmd) Assert(expected icmd.Expected) { 367 c.Base.T.Helper() 368 c.Run().Assert(c.Base.T, expected) 369 } 370 371 func (c *Cmd) AssertOK() { 372 c.Base.T.Helper() 373 c.AssertExitCode(0) 374 } 375 376 func (c *Cmd) AssertFail() { 377 c.Base.T.Helper() 378 res := c.Run() 379 assert.Assert(c.Base.T, res.ExitCode != 0) 380 } 381 382 func (c *Cmd) AssertExitCode(exitCode int) { 383 c.Base.T.Helper() 384 res := c.Run() 385 assert.Assert(c.Base.T, res.ExitCode == exitCode, res.Combined()) 386 } 387 388 func (c *Cmd) AssertOutContains(s string) { 389 c.Base.T.Helper() 390 expected := icmd.Expected{ 391 Out: s, 392 } 393 c.Assert(expected) 394 } 395 396 func (c *Cmd) AssertCombinedOutContains(s string) { 397 c.Base.T.Helper() 398 res := c.Run() 399 assert.Assert(c.Base.T, strings.Contains(res.Combined(), s), fmt.Sprintf("expected output to contain %q: %q", s, res.Combined())) 400 } 401 402 // AssertOutContainsAll checks if command output contains All strings in `strs`. 403 func (c *Cmd) AssertOutContainsAll(strs ...string) { 404 c.Base.T.Helper() 405 fn := func(stdout string) error { 406 for _, s := range strs { 407 if !strings.Contains(stdout, s) { 408 return fmt.Errorf("expected stdout to contain %q", s) 409 } 410 } 411 return nil 412 } 413 c.AssertOutWithFunc(fn) 414 } 415 416 // AssertOutContainsAny checks if command output contains Any string in `strs`. 417 func (c *Cmd) AssertOutContainsAny(strs ...string) { 418 c.Base.T.Helper() 419 fn := func(stdout string) error { 420 for _, s := range strs { 421 if strings.Contains(stdout, s) { 422 return nil 423 } 424 } 425 return fmt.Errorf("expected stdout to contain any of %q", strings.Join(strs, "|")) 426 } 427 c.AssertOutWithFunc(fn) 428 } 429 430 func (c *Cmd) AssertOutNotContains(s string) { 431 c.AssertOutWithFunc(func(stdout string) error { 432 if strings.Contains(stdout, s) { 433 return fmt.Errorf("expected stdout to not contain %q", s) 434 } 435 return nil 436 }) 437 } 438 439 func (c *Cmd) AssertOutExactly(s string) { 440 c.Base.T.Helper() 441 fn := func(stdout string) error { 442 if stdout != s { 443 return fmt.Errorf("expected %q, got %q", s, stdout) 444 } 445 return nil 446 } 447 c.AssertOutWithFunc(fn) 448 } 449 450 func (c *Cmd) AssertOutStreamsExactly(stdout, stderr string) { 451 c.Base.T.Helper() 452 fn := func(sout, serr string) error { 453 msg := "" 454 if sout != stdout { 455 msg += fmt.Sprintf("stdout mismatch, expected %q, got %q\n", stdout, sout) 456 } 457 if serr != stderr { 458 msg += fmt.Sprintf("stderr mismatch, expected %q, got %q\n", stderr, serr) 459 } 460 if msg != "" { 461 return fmt.Errorf("%s", msg) 462 } 463 return nil 464 } 465 c.AssertOutStreamsWithFunc(fn) 466 } 467 468 func (c *Cmd) AssertNoOut(s string) { 469 c.Base.T.Helper() 470 fn := func(stdout string) error { 471 if strings.Contains(stdout, s) { 472 return fmt.Errorf("expected not to contain %q, got %q", s, stdout) 473 } 474 return nil 475 } 476 c.AssertOutWithFunc(fn) 477 } 478 479 func (c *Cmd) AssertOutWithFunc(fn func(stdout string) error) { 480 c.Base.T.Helper() 481 res := c.Run() 482 assert.Equal(c.Base.T, 0, res.ExitCode, res.Combined()) 483 assert.NilError(c.Base.T, fn(res.Stdout()), res.Combined()) 484 } 485 486 func (c *Cmd) AssertOutStreamsWithFunc(fn func(stdout, stderr string) error) { 487 c.Base.T.Helper() 488 res := c.Run() 489 assert.Equal(c.Base.T, 0, res.ExitCode, res.Combined()) 490 assert.NilError(c.Base.T, fn(res.Stdout(), res.Stderr()), res.Combined()) 491 } 492 493 func (c *Cmd) Out() string { 494 c.Base.T.Helper() 495 res := c.Run() 496 assert.Equal(c.Base.T, 0, res.ExitCode, res.Combined()) 497 return res.Stdout() 498 } 499 500 func (c *Cmd) OutLines() []string { 501 c.Base.T.Helper() 502 out := c.Out() 503 // FIXME: improve memory efficiency 504 return strings.Split(out, "\n") 505 } 506 507 type Target = string 508 509 const ( 510 Nerdctl = Target("nerdctl") 511 Docker = Target("docker") 512 ) 513 514 var ( 515 flagTestTarget Target 516 flagTestKillDaemon bool 517 flagTestIPv6 bool 518 ) 519 520 func M(m *testing.M) { 521 flag.StringVar(&flagTestTarget, "test.target", Nerdctl, "target to test") 522 flag.BoolVar(&flagTestKillDaemon, "test.kill-daemon", false, "enable tests that kill the daemon") 523 flag.BoolVar(&flagTestIPv6, "test.ipv6", false, "enable tests on IPv6") 524 flag.Parse() 525 fmt.Fprintf(os.Stderr, "test target: %q\n", flagTestTarget) 526 os.Exit(m.Run()) 527 } 528 529 func GetTarget() string { 530 if flagTestTarget == "" { 531 panic("GetTarget() was called without calling M()") 532 } 533 return flagTestTarget 534 } 535 536 func GetEnableIPv6() bool { 537 return flagTestIPv6 538 } 539 540 func GetDaemonIsKillable() bool { 541 if flagTestKillDaemon && strings.HasPrefix(infoutil.DistroName(), "Ubuntu 24.04") { // FIXME: check systemd version, not distro 542 log.L.Warn("FIXME: Ignoring -test.kill-daemon: the flag does not seem to work on Ubuntu 24.04") 543 // > Failed to kill unit containerd.service: Failed to send signal SIGKILL to auxiliary processes: Invalid argument\n 544 // https://github.com/containerd/nerdctl/pull/3129#issuecomment-2185780506 545 return false 546 } 547 return flagTestKillDaemon 548 } 549 550 func DockerIncompatible(t testing.TB) { 551 if GetTarget() == Docker { 552 t.Skip("test is incompatible with Docker") 553 } 554 } 555 556 func RequiresBuild(t testing.TB) { 557 if GetTarget() == Nerdctl { 558 buildkitHost, err := buildkitutil.GetBuildkitHost(Namespace) 559 if err != nil { 560 t.Skipf("test requires buildkitd: %+v", err) 561 } 562 t.Logf("buildkitHost=%q", buildkitHost) 563 } 564 } 565 566 func RequireExecPlatform(t testing.TB, ss ...string) { 567 ok, err := platformutil.CanExecProbably(ss...) 568 if !ok { 569 msg := fmt.Sprintf("test requires platform %v", ss) 570 if err != nil { 571 msg += fmt.Sprintf(": %v", err) 572 } 573 t.Skip(msg) 574 } 575 } 576 577 func RequireDaemonVersion(b *Base, constraint string) { 578 b.T.Helper() 579 c, err := semver.NewConstraint(constraint) 580 if err != nil { 581 b.T.Fatal(err) 582 } 583 info := b.Info() 584 sv, err := semver.NewVersion(info.ServerVersion) 585 if err != nil { 586 b.T.Skip(err) 587 } 588 if !c.Check(sv) { 589 b.T.Skipf("version %v does not satisfy constraints %v", sv, c) 590 } 591 } 592 593 func RequireKernelVersion(t testing.TB, constraint string) { 594 t.Helper() 595 c, err := semver.NewConstraint(constraint) 596 if err != nil { 597 t.Fatal(err) 598 } 599 unameR, err := semver.NewVersion(infoutil.UnameR()) 600 if err != nil { 601 t.Skip(err) 602 } 603 if !c.Check(unameR) { 604 t.Skipf("version %v does not satisfy constraints %v", unameR, c) 605 } 606 } 607 608 func RequireContainerdPlugin(base *Base, requiredType, requiredID string, requiredCaps []string) { 609 base.T.Helper() 610 info := base.InfoNative() 611 for _, p := range info.Daemon.Plugins.Plugins { 612 if p.Type != requiredType { 613 continue 614 } 615 if p.ID != requiredID { 616 continue 617 } 618 pCapMap := make(map[string]struct{}, len(p.Capabilities)) 619 for _, f := range p.Capabilities { 620 pCapMap[f] = struct{}{} 621 } 622 for _, f := range requiredCaps { 623 if _, ok := pCapMap[f]; !ok { 624 base.T.Skipf("test requires containerd plugin \"%s.%s\" with capabilities %v (missing %q)", requiredType, requiredID, requiredCaps, f) 625 } 626 } 627 return 628 } 629 if len(requiredCaps) == 0 { 630 base.T.Skipf("test requires containerd plugin \"%s.%s\"", requiredType, requiredID) 631 } else { 632 base.T.Skipf("test requires containerd plugin \"%s.%s\" with capabilities %v", requiredType, requiredID, requiredCaps) 633 } 634 } 635 636 func RequireSystemService(t testing.TB, sv string) { 637 t.Helper() 638 if runtime.GOOS != "linux" { 639 t.Skipf("Service %q is not supported on %q", sv, runtime.GOOS) 640 } 641 var systemctlArgs []string 642 if rootlessutil.IsRootless() { 643 systemctlArgs = append(systemctlArgs, "--user") 644 } 645 systemctlArgs = append(systemctlArgs, []string{"-q", "is-active", sv}...) 646 cmd := exec.Command("systemctl", systemctlArgs...) 647 if err := cmd.Run(); err != nil { 648 t.Skipf("Service %q does not seem active: %v: %v", sv, cmd.Args, err) 649 } 650 } 651 652 // RequireExecutable skips tests when executable `name` is not present in PATH. 653 func RequireExecutable(t testing.TB, name string) { 654 if _, err := exec.LookPath(name); err != nil { 655 t.Skipf("required executable doesn't exist in PATH: %s", name) 656 } 657 } 658 659 const Namespace = "nerdctl-test" 660 661 func NewBaseWithNamespace(t *testing.T, ns string) *Base { 662 if ns == "" || ns == "default" || ns == Namespace { 663 t.Fatalf(`the other base namespace cannot be "%s"`, ns) 664 } 665 return newBase(t, ns, false) 666 } 667 668 func NewBaseWithIPv6Compatible(t *testing.T) *Base { 669 return newBase(t, Namespace, true) 670 } 671 672 func NewBase(t *testing.T) *Base { 673 return newBase(t, Namespace, false) 674 } 675 676 func newBase(t *testing.T, ns string, ipv6Compatible bool) *Base { 677 base := &Base{ 678 T: t, 679 Target: GetTarget(), 680 DaemonIsKillable: GetDaemonIsKillable(), 681 EnableIPv6: GetEnableIPv6(), 682 IPv6Compatible: ipv6Compatible, 683 } 684 if base.EnableIPv6 && !base.IPv6Compatible { 685 t.Skip("runner skips non-IPv6 complatible tests in the IPv6 environment") 686 } else if !base.EnableIPv6 && base.IPv6Compatible { 687 t.Skip("runner skips IPv6 compatible tests in the non-IPv6 environment") 688 } 689 var err error 690 switch base.Target { 691 case Nerdctl: 692 base.Binary, err = exec.LookPath("nerdctl") 693 if err != nil { 694 t.Fatal(err) 695 } 696 base.Args = []string{"--namespace=" + ns} 697 case Docker: 698 base.Binary, err = exec.LookPath("docker") 699 if err != nil { 700 t.Fatal(err) 701 } 702 if err := exec.Command("docker", "compose", "version").Run(); err != nil { 703 t.Fatal(err) 704 } 705 default: 706 t.Fatalf("unknown test target %q", base.Target) 707 } 708 return base 709 } 710 711 // Identifier can be used as a name of container, image, volume, network, etc. 712 func Identifier(t testing.TB) string { 713 s := t.Name() 714 s = strings.ReplaceAll(s, " ", "_") 715 s = strings.ReplaceAll(s, "/", "-") 716 s = strings.ToLower(s) 717 s = "nerdctl-" + s 718 if len(s) > 76 { 719 s = "nerdctl-" + digest.SHA256.FromString(t.Name()).Encoded() 720 } 721 return s 722 } 723 724 // ImageRepo returns the image repo that can be used to, e.g, validate output 725 // from `nerdctl images`. 726 func ImageRepo(s string) string { 727 repo, _ := imgutil.ParseRepoTag(s) 728 return repo 729 }