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 }