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  }