github.com/mvdan/u-root-coreutils@v0.0.0-20230122170626-c2eef2898555/pkg/vmtest/integration.go (about)

     1  // Copyright 2018 the u-root Authors. All rights reserved
     2  // Use of this source code is governed by a BSD-style
     3  // license that can be found in the LICENSE file.
     4  
     5  package vmtest
     6  
     7  import (
     8  	"fmt"
     9  	"io"
    10  	"log"
    11  	"os"
    12  	"path/filepath"
    13  	"runtime"
    14  	"strings"
    15  	"testing"
    16  
    17  	gbbgolang "github.com/u-root/gobusybox/src/pkg/golang"
    18  	"github.com/mvdan/u-root-coreutils/pkg/cp"
    19  	"github.com/mvdan/u-root-coreutils/pkg/qemu"
    20  	"github.com/mvdan/u-root-coreutils/pkg/testutil"
    21  	"github.com/mvdan/u-root-coreutils/pkg/uio"
    22  	"github.com/mvdan/u-root-coreutils/pkg/ulog"
    23  	"github.com/mvdan/u-root-coreutils/pkg/ulog/ulogtest"
    24  	"github.com/mvdan/u-root-coreutils/pkg/uroot"
    25  	"github.com/mvdan/u-root-coreutils/pkg/uroot/initramfs"
    26  )
    27  
    28  // Options are integration test options.
    29  type Options struct {
    30  	// BuildOpts are u-root initramfs options.
    31  	//
    32  	// They are used if the test needs to generate an initramfs.
    33  	// Fields that are not set are populated by QEMU and QEMUTest as
    34  	// possible.
    35  	BuildOpts uroot.Opts
    36  
    37  	// QEMUOpts are QEMU VM options for the test.
    38  	//
    39  	// Fields that are not set are populated by QEMU and QEMUTest as
    40  	// possible.
    41  	QEMUOpts qemu.Options
    42  
    43  	// Name is the test's name.
    44  	//
    45  	// If name is left empty, the calling function's function name will be
    46  	// used as determined by runtime.Caller.
    47  	Name string
    48  
    49  	// Uinit is the uinit that should be added to a generated initramfs.
    50  	//
    51  	// If none is specified, the generic uinit will be used, which searches for
    52  	// and runs the script generated from TestCmds.
    53  	Uinit string
    54  
    55  	// TestCmds are commands to execute after init.
    56  	//
    57  	// QEMUTest generates an Elvish script with these commands. The script is
    58  	// shared with the VM, and is run from the generic uinit.
    59  	TestCmds []string
    60  
    61  	// TmpDir is the temporary directory exposed to the QEMU VM.
    62  	TmpDir string
    63  
    64  	// Logger logs build statements.
    65  	Logger ulog.Logger
    66  
    67  	// Extra environment variables to set when building (used by u-bmc)
    68  	ExtraBuildEnv []string
    69  
    70  	// Use virtual vfat rather than 9pfs
    71  	UseVVFAT bool
    72  
    73  	// By default, if your kernel has CONFIG_DEBUG_FS=y and
    74  	// CONFIG_GCOV_KERNEL=y enabled, the kernel's coverage will be
    75  	// collected and saved to:
    76  	//   u-root/integration/coverage/{{testname}}/{{instance}}/kernel_coverage.tar
    77  	NoKernelCoverage bool
    78  }
    79  
    80  // Tests are run from u-root/integration/{gotests,generic-tests}/
    81  const coveragePath = "../coverage"
    82  
    83  // Keeps track of the number of instances per test so we do not overlap
    84  // coverage reports.
    85  var instance = map[string]int{}
    86  
    87  func last(s string) string {
    88  	l := strings.Split(s, ".")
    89  	return l[len(l)-1]
    90  }
    91  
    92  func callerName(depth int) string {
    93  	// Use the test name as the serial log's file name.
    94  	pc, _, _, ok := runtime.Caller(depth)
    95  	if !ok {
    96  		panic("runtime caller failed")
    97  	}
    98  	f := runtime.FuncForPC(pc)
    99  	return last(f.Name())
   100  }
   101  
   102  // TestLineWriter is an io.Writer that logs full lines of serial to tb.
   103  func TestLineWriter(tb testing.TB, prefix string) io.WriteCloser {
   104  	return uio.FullLineWriter(&testLineWriter{tb: tb, prefix: prefix})
   105  }
   106  
   107  type jsonStripper struct {
   108  	uio.LineWriter
   109  }
   110  
   111  func (j jsonStripper) OneLine(p []byte) {
   112  	// Poor man's JSON detector.
   113  	if len(p) == 0 || p[0] == '{' {
   114  		return
   115  	}
   116  	j.LineWriter.OneLine(p)
   117  }
   118  
   119  func JSONLessTestLineWriter(tb testing.TB, prefix string) io.WriteCloser {
   120  	return uio.FullLineWriter(jsonStripper{&testLineWriter{tb: tb, prefix: prefix}})
   121  }
   122  
   123  // testLineWriter is an io.Writer that logs full lines of serial to tb.
   124  type testLineWriter struct {
   125  	tb     testing.TB
   126  	prefix string
   127  }
   128  
   129  func replaceCtl(str []byte) []byte {
   130  	for i, c := range str {
   131  		if c == 9 || c == 10 {
   132  		} else if c < 32 || c == 127 {
   133  			str[i] = '~'
   134  		}
   135  	}
   136  	return str
   137  }
   138  
   139  func (tsw *testLineWriter) OneLine(p []byte) {
   140  	tsw.tb.Logf("%s %s: %s", testutil.NowLog(), tsw.prefix, string(replaceCtl(p)))
   141  }
   142  
   143  // TestArch returns the architecture under test. Pass this as GOARCH when
   144  // building Go programs to be run in the QEMU environment.
   145  func TestArch() string {
   146  	if env := os.Getenv("UROOT_TESTARCH"); env != "" {
   147  		return env
   148  	}
   149  	return "amd64"
   150  }
   151  
   152  // SkipWithoutQEMU skips the test when the QEMU environment variables are not
   153  // set. This is already called by QEMUTest(), so use if some expensive
   154  // operations are performed before calling QEMUTest().
   155  func SkipWithoutQEMU(t *testing.T) {
   156  	if _, ok := os.LookupEnv("UROOT_QEMU"); !ok {
   157  		t.Skip("QEMU test is skipped unless UROOT_QEMU is set")
   158  	}
   159  	if _, ok := os.LookupEnv("UROOT_KERNEL"); !ok {
   160  		t.Skip("QEMU test is skipped unless UROOT_KERNEL is set")
   161  	}
   162  }
   163  
   164  func saveCoverage(t *testing.T, path string) error {
   165  	// Coverage may not have been collected, for example if the kernel is
   166  	// not built with CONFIG_GCOV_KERNEL.
   167  	if fi, err := os.Stat(path); os.IsNotExist(err) || (err != nil && !fi.Mode().IsRegular()) {
   168  		return nil
   169  	}
   170  
   171  	// Move coverage to common directory.
   172  	uniqueCoveragePath := filepath.Join(coveragePath, t.Name(), fmt.Sprintf("%d", instance[t.Name()]))
   173  	instance[t.Name()]++
   174  	if err := os.MkdirAll(uniqueCoveragePath, 0o770); err != nil {
   175  		return err
   176  	}
   177  	if err := os.Rename(path, filepath.Join(uniqueCoveragePath, filepath.Base(path))); err != nil {
   178  		return err
   179  	}
   180  	return nil
   181  }
   182  
   183  func QEMUTest(t *testing.T, o *Options) (*qemu.VM, func()) {
   184  	SkipWithoutQEMU(t)
   185  
   186  	// Delete any previous coverage data.
   187  	if _, ok := instance[t.Name()]; !ok {
   188  		testCoveragePath := filepath.Join(coveragePath, t.Name())
   189  		if err := os.RemoveAll(testCoveragePath); err != nil && !os.IsNotExist(err) {
   190  			t.Logf("Error erasing previous coverage: %v", err)
   191  		}
   192  	}
   193  
   194  	if len(o.Name) == 0 {
   195  		o.Name = callerName(2)
   196  	}
   197  	if o.Logger == nil {
   198  		o.Logger = &ulogtest.Logger{TB: t}
   199  	}
   200  	if o.QEMUOpts.SerialOutput == nil {
   201  		o.QEMUOpts.SerialOutput = TestLineWriter(t, "serial")
   202  	}
   203  
   204  	// Create or reuse a temporary directory. This is exposed to the VM.
   205  	if o.TmpDir == "" {
   206  		tmpDir, err := os.MkdirTemp("", "uroot-integration")
   207  		if err != nil {
   208  			t.Fatalf("Failed to create temp dir: %v", err)
   209  		}
   210  		o.TmpDir = tmpDir
   211  	}
   212  
   213  	qOpts, err := QEMU(o)
   214  	if err != nil {
   215  		t.Fatalf("Failed to create QEMU VM %s: %v", o.Name, err)
   216  	}
   217  
   218  	vm, err := qOpts.Start()
   219  	if err != nil {
   220  		t.Fatalf("Failed to start QEMU VM %s: %v", o.Name, err)
   221  	}
   222  
   223  	return vm, func() {
   224  		vm.Close()
   225  		if !o.NoKernelCoverage {
   226  			if err := saveCoverage(t, filepath.Join(o.TmpDir, "kernel_coverage.tar")); err != nil {
   227  				t.Logf("Error saving kernel coverage: %v", err)
   228  			}
   229  		}
   230  
   231  		t.Logf("QEMU command line to reproduce %s:\n%s", o.Name, vm.CmdlineQuoted())
   232  		if t.Failed() {
   233  			t.Log("Keeping temp dir: ", o.TmpDir)
   234  		} else if len(o.TmpDir) == 0 {
   235  			if err := os.RemoveAll(o.TmpDir); err != nil {
   236  				t.Logf("failed to remove temporary directory %s: %v", o.TmpDir, err)
   237  			}
   238  		}
   239  	}
   240  }
   241  
   242  // QEMU builds the u-root environment and prepares QEMU options given the test
   243  // options and environment variables.
   244  //
   245  // QEMU will augment o.BuildOpts and o.QEMUOpts with configuration that the
   246  // caller either requested (through the Options.Uinit field, for example) or
   247  // that the caller did not set.
   248  //
   249  // QEMU returns the QEMU launch options or an error.
   250  func QEMU(o *Options) (*qemu.Options, error) {
   251  	if len(o.Name) == 0 {
   252  		o.Name = callerName(2)
   253  	}
   254  
   255  	// Generate Elvish shell script of test commands in o.TmpDir.
   256  	if len(o.TestCmds) > 0 {
   257  		testFile := filepath.Join(o.TmpDir, "test.elv")
   258  
   259  		if err := os.WriteFile(
   260  			testFile, []byte(strings.Join(o.TestCmds, "\n")), 0o777); err != nil {
   261  			return nil, err
   262  		}
   263  	}
   264  
   265  	// Set the initramfs.
   266  	if len(o.QEMUOpts.Initramfs) == 0 {
   267  		o.QEMUOpts.Initramfs = filepath.Join(o.TmpDir, "initramfs.cpio")
   268  		if err := ChooseTestInitramfs(o.BuildOpts, o.Uinit, o.QEMUOpts.Initramfs); err != nil {
   269  			return nil, err
   270  		}
   271  	}
   272  
   273  	if len(o.QEMUOpts.Kernel) == 0 {
   274  		// Copy kernel to o.TmpDir for tests involving kexec.
   275  		kernel := filepath.Join(o.TmpDir, "kernel")
   276  		if err := cp.Copy(os.Getenv("UROOT_KERNEL"), kernel); err != nil {
   277  			return nil, err
   278  		}
   279  		o.QEMUOpts.Kernel = kernel
   280  	}
   281  
   282  	switch TestArch() {
   283  	case "amd64":
   284  		o.QEMUOpts.KernelArgs += " console=ttyS0 earlyprintk=ttyS0"
   285  	case "arm":
   286  		o.QEMUOpts.KernelArgs += " console=ttyAMA0"
   287  	}
   288  	o.QEMUOpts.KernelArgs += " uroot.vmtest"
   289  
   290  	var dir qemu.Device
   291  	if o.UseVVFAT {
   292  		dir = qemu.ReadOnlyDirectory{Dir: o.TmpDir}
   293  	} else {
   294  		dir = qemu.P9Directory{Dir: o.TmpDir, Arch: TestArch()}
   295  	}
   296  	o.QEMUOpts.Devices = append(o.QEMUOpts.Devices, qemu.VirtioRandom{}, dir)
   297  
   298  	if o.NoKernelCoverage {
   299  		o.QEMUOpts.KernelArgs += " UROOT_NO_KERNEL_COVERAGE=1"
   300  	}
   301  
   302  	return &o.QEMUOpts, nil
   303  }
   304  
   305  // ChooseTestInitramfs chooses which initramfs will be used for a given test and
   306  // places it at the location given by outputFile.
   307  // Default to the override initramfs if one is specified in the UROOT_INITRAMFS
   308  // environment variable. Else, build an initramfs with the given parameters.
   309  // If no uinit was provided, the generic one is used.
   310  func ChooseTestInitramfs(o uroot.Opts, uinit, outputFile string) error {
   311  	override := os.Getenv("UROOT_INITRAMFS")
   312  	if len(override) > 0 {
   313  		log.Printf("Overriding with initramfs %q", override)
   314  		return cp.Copy(override, outputFile)
   315  	}
   316  
   317  	if len(uinit) == 0 {
   318  		log.Printf("Defaulting to generic initramfs")
   319  		uinit = "github.com/mvdan/u-root-coreutils/integration/testcmd/generic/uinit"
   320  	}
   321  
   322  	_, err := CreateTestInitramfs(o, uinit, outputFile)
   323  	return err
   324  }
   325  
   326  // CreateTestInitramfs creates an initramfs with the given build options and
   327  // uinit, and writes it to the given output file. If no output file is provided,
   328  // one will be created.
   329  // The output file name is returned. It is the caller's responsibility to remove
   330  // the initramfs file after use.
   331  func CreateTestInitramfs(o uroot.Opts, uinit, outputFile string) (string, error) {
   332  	if o.Env == nil {
   333  		env := gbbgolang.Default()
   334  		env.CgoEnabled = false
   335  		env.GOARCH = TestArch()
   336  		o.Env = &env
   337  	}
   338  
   339  	if o.UrootSource == "" {
   340  		sourcePath, ok := os.LookupEnv("UROOT_SOURCE")
   341  		if !ok {
   342  			return "", fmt.Errorf("failed to get u-root source directory, please set UROOT_SOURCE to the absolute path of the u-root source directory")
   343  		}
   344  		o.UrootSource = sourcePath
   345  	}
   346  
   347  	logger := log.New(os.Stderr, "", 0)
   348  
   349  	// If build opts don't specify any commands, include all commands. Else,
   350  	// always add init and elvish.
   351  	var cmds []string
   352  	if len(o.Commands) == 0 {
   353  		cmds = []string{
   354  			"github.com/mvdan/u-root-coreutils/cmds/core/*",
   355  			"github.com/mvdan/u-root-coreutils/cmds/exp/*",
   356  		}
   357  	}
   358  
   359  	if len(uinit) != 0 {
   360  		cmds = append(cmds, uinit)
   361  	}
   362  
   363  	// Add our commands to the build opts.
   364  	o.AddBusyBoxCommands(cmds...)
   365  
   366  	// Fill in the default build options if not specified.
   367  	if o.BaseArchive == nil {
   368  		o.BaseArchive = uroot.DefaultRamfs().Reader()
   369  	}
   370  	if len(o.InitCmd) == 0 {
   371  		o.InitCmd = "init"
   372  	}
   373  	if len(o.DefaultShell) == 0 {
   374  		o.DefaultShell = "elvish"
   375  	}
   376  	if len(o.TempDir) == 0 {
   377  		tempDir, err := os.MkdirTemp("", "initramfs-tempdir")
   378  		if err != nil {
   379  			return "", fmt.Errorf("Failed to create temp dir: %v", err)
   380  		}
   381  		defer os.RemoveAll(tempDir)
   382  		o.TempDir = tempDir
   383  	}
   384  
   385  	// Create an output file if one was not provided.
   386  	if len(outputFile) == 0 {
   387  		f, err := os.CreateTemp("", "initramfs.cpio")
   388  		if err != nil {
   389  			return "", fmt.Errorf("failed to create output file: %v", err)
   390  		}
   391  		outputFile = f.Name()
   392  	}
   393  	w, err := initramfs.CPIO.OpenWriter(logger, outputFile)
   394  	if err != nil {
   395  		return "", fmt.Errorf("Failed to create initramfs writer: %v", err)
   396  	}
   397  	o.OutputFile = w
   398  
   399  	return outputFile, uroot.CreateInitramfs(logger, o)
   400  }