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