github.com/xyproto/u-root@v6.0.1-0.20200302025726-5528e0c77a3c+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 (tsw *testLineWriter) OneLine(p []byte) {
   123  	tsw.tb.Logf("%s %s: %s", testutil.NowLog(), tsw.prefix, strings.ReplaceAll(string(p), "\033", "~"))
   124  }
   125  
   126  // TestArch returns the architecture under test. Pass this as GOARCH when
   127  // building Go programs to be run in the QEMU environment.
   128  func TestArch() string {
   129  	if env := os.Getenv("UROOT_TESTARCH"); env != "" {
   130  		return env
   131  	}
   132  	return "amd64"
   133  }
   134  
   135  // SkipWithoutQEMU skips the test when the QEMU environment variables are not
   136  // set. This is already called by QEMUTest(), so use if some expensive
   137  // operations are performed before calling QEMUTest().
   138  func SkipWithoutQEMU(t *testing.T) {
   139  	if _, ok := os.LookupEnv("UROOT_QEMU"); !ok {
   140  		t.Skip("QEMU test is skipped unless UROOT_QEMU is set")
   141  	}
   142  	if _, ok := os.LookupEnv("UROOT_KERNEL"); !ok {
   143  		t.Skip("QEMU test is skipped unless UROOT_KERNEL is set")
   144  	}
   145  }
   146  
   147  func QEMUTest(t *testing.T, o *Options) (*qemu.VM, func()) {
   148  	SkipWithoutQEMU(t)
   149  
   150  	if len(o.Name) == 0 {
   151  		o.Name = callerName(2)
   152  	}
   153  	if o.Logger == nil {
   154  		o.Logger = &ulogtest.Logger{TB: t}
   155  	}
   156  	if o.QEMUOpts.SerialOutput == nil {
   157  		o.QEMUOpts.SerialOutput = TestLineWriter(t, "serial")
   158  	}
   159  
   160  	// Create or reuse a temporary directory. This is exposed to the VM.
   161  	if o.TmpDir == "" {
   162  		tmpDir, err := ioutil.TempDir("", "uroot-integration")
   163  		if err != nil {
   164  			t.Fatalf("Failed to create temp dir: %v", err)
   165  		}
   166  		o.TmpDir = tmpDir
   167  	}
   168  
   169  	qOpts, err := QEMU(o)
   170  	if err != nil {
   171  		t.Fatalf("Failed to create QEMU VM %s: %v", o.Name, err)
   172  	}
   173  
   174  	vm, err := qOpts.Start()
   175  	if err != nil {
   176  		t.Fatalf("Failed to start QEMU VM %s: %v", o.Name, err)
   177  	}
   178  
   179  	return vm, func() {
   180  		vm.Close()
   181  		t.Logf("QEMU command line to reproduce %s:\n%s", o.Name, vm.CmdlineQuoted())
   182  		if t.Failed() {
   183  			t.Log("Keeping temp dir: ", o.TmpDir)
   184  		} else if len(o.TmpDir) == 0 {
   185  			if err := os.RemoveAll(o.TmpDir); err != nil {
   186  				t.Logf("failed to remove temporary directory %s: %v", o.TmpDir, err)
   187  			}
   188  		}
   189  	}
   190  }
   191  
   192  // QEMU builds the u-root environment and prepares QEMU options given the test
   193  // options and environment variables.
   194  //
   195  // QEMU will augment o.BuildOpts and o.QEMUOpts with configuration that the
   196  // caller either requested (through the Options.Uinit field, for example) or
   197  // that the caller did not set.
   198  //
   199  // QEMU returns the QEMU launch options or an error.
   200  func QEMU(o *Options) (*qemu.Options, error) {
   201  	if len(o.Name) == 0 {
   202  		o.Name = callerName(2)
   203  	}
   204  
   205  	// Generate Elvish shell script of test commands in o.TmpDir.
   206  	if len(o.TestCmds) > 0 {
   207  		testFile := filepath.Join(o.TmpDir, "test.elv")
   208  
   209  		if err := ioutil.WriteFile(
   210  			testFile, []byte(strings.Join(o.TestCmds, "\n")), 0777); err != nil {
   211  			return nil, err
   212  		}
   213  	}
   214  
   215  	// Set the initramfs.
   216  	if len(o.QEMUOpts.Initramfs) == 0 {
   217  		o.QEMUOpts.Initramfs = filepath.Join(o.TmpDir, "initramfs.cpio")
   218  		if err := ChooseTestInitramfs(o.DontSetEnv, o.BuildOpts, o.Uinit, o.QEMUOpts.Initramfs); err != nil {
   219  			return nil, err
   220  		}
   221  	}
   222  
   223  	if len(o.QEMUOpts.Kernel) == 0 {
   224  		// Copy kernel to o.TmpDir for tests involving kexec.
   225  		kernel := filepath.Join(o.TmpDir, "kernel")
   226  		if err := cp.Copy(os.Getenv("UROOT_KERNEL"), kernel); err != nil {
   227  			return nil, err
   228  		}
   229  		o.QEMUOpts.Kernel = kernel
   230  	}
   231  
   232  	switch TestArch() {
   233  	case "amd64":
   234  		o.QEMUOpts.KernelArgs += " console=ttyS0 earlyprintk=ttyS0"
   235  	case "arm":
   236  		o.QEMUOpts.KernelArgs += " console=ttyAMA0"
   237  	}
   238  
   239  	var dir qemu.Device
   240  	if o.UseVVFAT {
   241  		dir = qemu.ReadOnlyDirectory{Dir: o.TmpDir}
   242  	} else {
   243  		dir = qemu.P9Directory{Dir: o.TmpDir, Arch: TestArch()}
   244  	}
   245  	o.QEMUOpts.Devices = append(o.QEMUOpts.Devices, qemu.VirtioRandom{}, dir)
   246  
   247  	return &o.QEMUOpts, nil
   248  }
   249  
   250  // ChooseTestInitramfs chooses which initramfs will be used for a given test and
   251  // places it at the location given by outputFile.
   252  // Default to the override initramfs if one is specified in the UROOT_INITRAMFS
   253  // environment variable. Else, build an initramfs with the given parameters.
   254  // If no uinit was provided, the generic one is used.
   255  func ChooseTestInitramfs(dontSetEnv bool, o uroot.Opts, uinit, outputFile string) error {
   256  	override := os.Getenv("UROOT_INITRAMFS")
   257  	if len(override) > 0 {
   258  		log.Printf("Overriding with initramfs %q", override)
   259  		return cp.Copy(override, outputFile)
   260  	}
   261  
   262  	if len(uinit) == 0 {
   263  		log.Printf("Defaulting to generic initramfs")
   264  		uinit = "github.com/u-root/u-root/integration/testcmd/generic/uinit"
   265  	}
   266  
   267  	_, err := CreateTestInitramfs(dontSetEnv, o, uinit, outputFile)
   268  	return err
   269  }
   270  
   271  // CreateTestInitramfs creates an initramfs with the given build options and
   272  // uinit, and writes it to the given output file. If no output file is provided,
   273  // one will be created.
   274  // The output file name is returned. It is the caller's responsibility to remove
   275  // the initramfs file after use.
   276  func CreateTestInitramfs(dontSetEnv bool, o uroot.Opts, uinit, outputFile string) (string, error) {
   277  	if !dontSetEnv {
   278  		env := golang.Default()
   279  		env.CgoEnabled = false
   280  		env.GOARCH = TestArch()
   281  		o.Env = env
   282  	}
   283  
   284  	logger := log.New(os.Stderr, "", 0)
   285  
   286  	// If build opts don't specify any commands, include all commands. Else,
   287  	// always add init and elvish.
   288  	var cmds []string
   289  	if len(o.Commands) == 0 {
   290  		cmds = []string{
   291  			"github.com/u-root/u-root/cmds/core/*",
   292  			"github.com/u-root/u-root/cmds/exp/*",
   293  		}
   294  	}
   295  
   296  	if len(uinit) != 0 {
   297  		cmds = append(cmds, uinit)
   298  	}
   299  
   300  	// Add our commands to the build opts.
   301  	o.AddBusyBoxCommands(cmds...)
   302  
   303  	// Fill in the default build options if not specified.
   304  	if o.BaseArchive == nil {
   305  		o.BaseArchive = uroot.DefaultRamfs.Reader()
   306  	}
   307  	if len(o.InitCmd) == 0 {
   308  		o.InitCmd = "init"
   309  	}
   310  	if len(o.DefaultShell) == 0 {
   311  		o.DefaultShell = "elvish"
   312  	}
   313  	if len(o.TempDir) == 0 {
   314  		tempDir, err := ioutil.TempDir("", "initramfs-tempdir")
   315  		if err != nil {
   316  			return "", fmt.Errorf("Failed to create temp dir: %v", err)
   317  		}
   318  		defer os.RemoveAll(tempDir)
   319  		o.TempDir = tempDir
   320  	}
   321  
   322  	// Create an output file if one was not provided.
   323  	if len(outputFile) == 0 {
   324  		f, err := ioutil.TempFile("", "initramfs.cpio")
   325  		if err != nil {
   326  			return "", fmt.Errorf("failed to create output file: %v", err)
   327  		}
   328  		outputFile = f.Name()
   329  	}
   330  	w, err := initramfs.CPIO.OpenWriter(logger, outputFile, "", "")
   331  	if err != nil {
   332  		return "", fmt.Errorf("Failed to create initramfs writer: %v", err)
   333  	}
   334  	o.OutputFile = w
   335  
   336  	return outputFile, uroot.CreateInitramfs(logger, o)
   337  }