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