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