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