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