github.com/shaardie/u-root@v4.0.1-0.20190127173353-f24a1c26aa2e+incompatible/integration/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 integration
     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  	"time"
    19  
    20  	"github.com/u-root/u-root/pkg/cp"
    21  	"github.com/u-root/u-root/pkg/golang"
    22  	"github.com/u-root/u-root/pkg/qemu"
    23  	"github.com/u-root/u-root/pkg/uroot"
    24  	"github.com/u-root/u-root/pkg/uroot/builder"
    25  	"github.com/u-root/u-root/pkg/uroot/initramfs"
    26  	"github.com/u-root/u-root/pkg/uroot/logger"
    27  )
    28  
    29  // Serial output is written to this directory and picked up by circleci, or
    30  // you, if you want to read the serial logs.
    31  const logDir = "serial"
    32  
    33  const template = `
    34  package main
    35  
    36  import (
    37  	"log"
    38  	"os"
    39  	"os/exec"
    40  )
    41  
    42  func main() {
    43  	for _, cmds := range %#v {
    44  		cmd := exec.Command(cmds[0], cmds[1:]...)
    45  		cmd.Stdout = os.Stdout
    46  		cmd.Stderr = os.Stderr
    47  		err := cmd.Run()
    48  		if err != nil {
    49  			log.Fatal(err)
    50  		}
    51  	}
    52  }
    53  `
    54  
    55  // Options are integration test options.
    56  type Options struct {
    57  	// Env is the Go environment to use to build u-root.
    58  	Env *golang.Environ
    59  
    60  	// Name is the test's name.
    61  	//
    62  	// If name is left empty, the calling function's function name will be
    63  	// used as determined by runtime.Caller
    64  	Name string
    65  
    66  	// Go commands to include in the initramfs for the VM.
    67  	//
    68  	// If left empty, all u-root commands will be included.
    69  	Cmds []string
    70  
    71  	// Uinit are commands to execute after init.
    72  	//
    73  	// If populated, a uinit.go will be generated from these.
    74  	Uinit []string
    75  
    76  	// Files are files to include in the VMs initramfs.
    77  	Files []string
    78  
    79  	// TmpDir is a temporary directory for build artifacts.
    80  	TmpDir string
    81  
    82  	// LogFile is a file to log serial output to.
    83  	//
    84  	// The default is serial/$Name.log
    85  	LogFile string
    86  
    87  	// Logger logs build statements.
    88  	Logger logger.Logger
    89  
    90  	// Timeout is the timeout for expect statements.
    91  	Timeout time.Duration
    92  
    93  	// Network is the VM's network.
    94  	Network *qemu.Network
    95  
    96  	// Extra environment variables to set when building (used by u-bmc)
    97  	ExtraBuildEnv []string
    98  
    99  	// Serial Output
   100  	SerialOutput io.WriteCloser
   101  }
   102  
   103  func last(s string) string {
   104  	l := strings.Split(s, ".")
   105  	return l[len(l)-1]
   106  }
   107  
   108  type testLogger struct {
   109  	t *testing.T
   110  }
   111  
   112  func (tl testLogger) Printf(format string, v ...interface{}) {
   113  	tl.t.Logf(format, v...)
   114  }
   115  
   116  func (tl testLogger) Print(v ...interface{}) {
   117  	tl.t.Log(v...)
   118  }
   119  
   120  func callerName(depth int) string {
   121  	// Use the test name as the serial log's file name.
   122  	pc, _, _, ok := runtime.Caller(depth)
   123  	if !ok {
   124  		panic("runtime caller failed")
   125  	}
   126  	f := runtime.FuncForPC(pc)
   127  	return last(f.Name())
   128  }
   129  
   130  // TestArch returns the architecture under test. Pass this as GOARCH when
   131  // building Go programs to be run in the QEMU environment.
   132  func TestArch() string {
   133  	if env := os.Getenv("UROOT_TESTARCH"); env != "" {
   134  		return env
   135  	}
   136  	return "amd64"
   137  }
   138  
   139  // SkipWithoutQEMU skips the test when the QEMU environment variables are not
   140  // set. This is already called by QEMUTest(), so use if some expensive
   141  // operations are performed before calling QEMUTest().
   142  func SkipWithoutQEMU(t *testing.T) {
   143  	if _, ok := os.LookupEnv("UROOT_QEMU"); !ok {
   144  		t.Skip("QEMU test is skipped unless UROOT_QEMU is set")
   145  	}
   146  	if _, ok := os.LookupEnv("UROOT_KERNEL"); !ok {
   147  		t.Skip("QEMU test is skipped unless UROOT_KERNEL is set")
   148  	}
   149  }
   150  
   151  func QEMUTest(t *testing.T, o *Options) (*qemu.VM, func()) {
   152  	SkipWithoutQEMU(t)
   153  
   154  	if len(o.Name) == 0 {
   155  		o.Name = callerName(2)
   156  	}
   157  	if o.Logger == nil {
   158  		o.Logger = &testLogger{t}
   159  	}
   160  
   161  	qOpts, err := QEMU(o)
   162  	if err != nil {
   163  		t.Fatalf("Failed to create QEMU VM %s: %v", o.Name, err)
   164  	}
   165  
   166  	vm, err := qOpts.Start()
   167  	if err != nil {
   168  		t.Fatalf("Failed to start QEMU VM %s: %v", o.Name, err)
   169  	}
   170  	t.Logf("QEMU command line for %s:\n%s", o.Name, vm.CmdlineQuoted())
   171  
   172  	return vm, func() {
   173  		vm.Close()
   174  		dir := vm.Options.Devices[0].(qemu.ReadOnlyDirectory).Dir
   175  		if t.Failed() {
   176  			t.Log("keeping temp dir: ", dir)
   177  		} else if len(o.TmpDir) == 0 {
   178  			if err := os.RemoveAll(dir); err != nil {
   179  				t.Logf("failed to remove temporary directory %s: %v", dir, err)
   180  			}
   181  		}
   182  	}
   183  }
   184  
   185  func QEMU(o *Options) (*qemu.Options, error) {
   186  	if len(o.Name) == 0 {
   187  		o.Name = callerName(2)
   188  	}
   189  
   190  	if o.Env == nil {
   191  		env := golang.Default()
   192  		o.Env = &env
   193  		o.Env.CgoEnabled = false
   194  		env.GOARCH = TestArch()
   195  	}
   196  
   197  	if len(o.LogFile) == 0 {
   198  		// Create file for serial logs.
   199  		if err := os.MkdirAll(logDir, 0755); err != nil {
   200  			return nil, fmt.Errorf("could not create serial log directory: %v", err)
   201  		}
   202  
   203  		o.LogFile = filepath.Join(logDir, fmt.Sprintf("%s.log", o.Name))
   204  	}
   205  
   206  	var cmds []string
   207  	if len(o.Cmds) == 0 {
   208  		cmds = append(cmds, "github.com/u-root/u-root/cmds/*")
   209  	} else {
   210  		cmds = append(cmds, o.Cmds...)
   211  	}
   212  	// Create a uinit from the commands given.
   213  	if len(o.Uinit) > 0 {
   214  		urootPkg, err := o.Env.Package("github.com/u-root/u-root/integration")
   215  		if err != nil {
   216  			return nil, err
   217  		}
   218  		testDir := filepath.Join(urootPkg.Dir, "testcmd")
   219  
   220  		dirpath, err := ioutil.TempDir(testDir, "uinit-")
   221  		if err != nil {
   222  			return nil, err
   223  		}
   224  		defer os.RemoveAll(dirpath)
   225  
   226  		if err := os.MkdirAll(filepath.Join(dirpath, "uinit"), 0755); err != nil {
   227  			return nil, err
   228  		}
   229  
   230  		var realUinit [][]string
   231  		for _, cmd := range o.Uinit {
   232  			realUinit = append(realUinit, fields(cmd))
   233  		}
   234  
   235  		if err := ioutil.WriteFile(
   236  			filepath.Join(dirpath, "uinit", "uinit.go"),
   237  			[]byte(fmt.Sprintf(template, realUinit)),
   238  			0755); err != nil {
   239  			return nil, err
   240  		}
   241  		cmds = append(cmds, path.Join("github.com/u-root/u-root/integration/testcmd", filepath.Base(dirpath), "uinit"))
   242  	}
   243  
   244  	// Create or reuse a temporary directory.
   245  	tmpDir := o.TmpDir
   246  	if len(tmpDir) == 0 {
   247  		var err error
   248  		tmpDir, err = ioutil.TempDir("", "uroot-integration")
   249  		if err != nil {
   250  			return nil, err
   251  		}
   252  	}
   253  
   254  	if o.Logger == nil {
   255  		o.Logger = log.New(os.Stderr, "", 0)
   256  	}
   257  
   258  	// OutputFile
   259  	outputFile := filepath.Join(tmpDir, "initramfs.cpio")
   260  	w, err := initramfs.CPIO.OpenWriter(o.Logger, outputFile, "", "")
   261  	if err != nil {
   262  		return nil, err
   263  	}
   264  
   265  	// Build u-root
   266  	opts := uroot.Opts{
   267  		Env: *o.Env,
   268  		Commands: []uroot.Commands{
   269  			{
   270  				Builder:  builder.BusyBox,
   271  				Packages: cmds,
   272  			},
   273  		},
   274  		ExtraFiles:   o.Files,
   275  		TempDir:      tmpDir,
   276  		BaseArchive:  uroot.DefaultRamfs.Reader(),
   277  		OutputFile:   w,
   278  		InitCmd:      "init",
   279  		DefaultShell: "elvish",
   280  	}
   281  	if err := uroot.CreateInitramfs(o.Logger, opts); err != nil {
   282  		return nil, err
   283  	}
   284  
   285  	// Copy kernel to tmpDir.
   286  	bzImage := filepath.Join(tmpDir, "bzImage")
   287  	if err := cp.Copy(os.Getenv("UROOT_KERNEL"), bzImage); err != nil {
   288  		return nil, err
   289  	}
   290  
   291  	logFile := o.SerialOutput
   292  	if logFile == nil {
   293  		if o.LogFile != "" {
   294  			logFile, err = os.Create(o.LogFile)
   295  			if err != nil {
   296  				return nil, fmt.Errorf("could not create log file: %v", err)
   297  			}
   298  		}
   299  	}
   300  
   301  	kernelArgs := ""
   302  	switch TestArch() {
   303  	case "amd64":
   304  		kernelArgs = "console=ttyS0 earlyprintk=ttyS0"
   305  	case "arm":
   306  		kernelArgs = "console=ttyAMA0"
   307  	}
   308  
   309  	return &qemu.Options{
   310  		Initramfs:    outputFile,
   311  		Kernel:       bzImage,
   312  		KernelArgs:   kernelArgs,
   313  		SerialOutput: logFile,
   314  		Timeout:      o.Timeout,
   315  		Devices: []qemu.Device{
   316  			qemu.ReadOnlyDirectory{Dir: tmpDir},
   317  			qemu.VirtioRandom{},
   318  			o.Network,
   319  		},
   320  	}, nil
   321  }