github.com/mvdan/u-root-coreutils@v0.0.0-20230122170626-c2eef2898555/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 "log" 11 "os" 12 "path/filepath" 13 "runtime" 14 "strings" 15 "testing" 16 17 gbbgolang "github.com/u-root/gobusybox/src/pkg/golang" 18 "github.com/mvdan/u-root-coreutils/pkg/cp" 19 "github.com/mvdan/u-root-coreutils/pkg/qemu" 20 "github.com/mvdan/u-root-coreutils/pkg/testutil" 21 "github.com/mvdan/u-root-coreutils/pkg/uio" 22 "github.com/mvdan/u-root-coreutils/pkg/ulog" 23 "github.com/mvdan/u-root-coreutils/pkg/ulog/ulogtest" 24 "github.com/mvdan/u-root-coreutils/pkg/uroot" 25 "github.com/mvdan/u-root-coreutils/pkg/uroot/initramfs" 26 ) 27 28 // Options are integration test options. 29 type Options struct { 30 // BuildOpts are u-root initramfs options. 31 // 32 // They are used if the test needs to generate an initramfs. 33 // Fields that are not set are populated by QEMU and QEMUTest as 34 // possible. 35 BuildOpts uroot.Opts 36 37 // QEMUOpts are QEMU VM options for the test. 38 // 39 // Fields that are not set are populated by QEMU and QEMUTest as 40 // possible. 41 QEMUOpts qemu.Options 42 43 // Name is the test's name. 44 // 45 // If name is left empty, the calling function's function name will be 46 // used as determined by runtime.Caller. 47 Name string 48 49 // Uinit is the uinit that should be added to a generated initramfs. 50 // 51 // If none is specified, the generic uinit will be used, which searches for 52 // and runs the script generated from TestCmds. 53 Uinit string 54 55 // TestCmds are commands to execute after init. 56 // 57 // QEMUTest generates an Elvish script with these commands. The script is 58 // shared with the VM, and is run from the generic uinit. 59 TestCmds []string 60 61 // TmpDir is the temporary directory exposed to the QEMU VM. 62 TmpDir string 63 64 // Logger logs build statements. 65 Logger ulog.Logger 66 67 // Extra environment variables to set when building (used by u-bmc) 68 ExtraBuildEnv []string 69 70 // Use virtual vfat rather than 9pfs 71 UseVVFAT bool 72 73 // By default, if your kernel has CONFIG_DEBUG_FS=y and 74 // CONFIG_GCOV_KERNEL=y enabled, the kernel's coverage will be 75 // collected and saved to: 76 // u-root/integration/coverage/{{testname}}/{{instance}}/kernel_coverage.tar 77 NoKernelCoverage bool 78 } 79 80 // Tests are run from u-root/integration/{gotests,generic-tests}/ 81 const coveragePath = "../coverage" 82 83 // Keeps track of the number of instances per test so we do not overlap 84 // coverage reports. 85 var instance = map[string]int{} 86 87 func last(s string) string { 88 l := strings.Split(s, ".") 89 return l[len(l)-1] 90 } 91 92 func callerName(depth int) string { 93 // Use the test name as the serial log's file name. 94 pc, _, _, ok := runtime.Caller(depth) 95 if !ok { 96 panic("runtime caller failed") 97 } 98 f := runtime.FuncForPC(pc) 99 return last(f.Name()) 100 } 101 102 // TestLineWriter is an io.Writer that logs full lines of serial to tb. 103 func TestLineWriter(tb testing.TB, prefix string) io.WriteCloser { 104 return uio.FullLineWriter(&testLineWriter{tb: tb, prefix: prefix}) 105 } 106 107 type jsonStripper struct { 108 uio.LineWriter 109 } 110 111 func (j jsonStripper) OneLine(p []byte) { 112 // Poor man's JSON detector. 113 if len(p) == 0 || p[0] == '{' { 114 return 115 } 116 j.LineWriter.OneLine(p) 117 } 118 119 func JSONLessTestLineWriter(tb testing.TB, prefix string) io.WriteCloser { 120 return uio.FullLineWriter(jsonStripper{&testLineWriter{tb: tb, prefix: prefix}}) 121 } 122 123 // testLineWriter is an io.Writer that logs full lines of serial to tb. 124 type testLineWriter struct { 125 tb testing.TB 126 prefix string 127 } 128 129 func replaceCtl(str []byte) []byte { 130 for i, c := range str { 131 if c == 9 || c == 10 { 132 } else if c < 32 || c == 127 { 133 str[i] = '~' 134 } 135 } 136 return str 137 } 138 139 func (tsw *testLineWriter) OneLine(p []byte) { 140 tsw.tb.Logf("%s %s: %s", testutil.NowLog(), tsw.prefix, string(replaceCtl(p))) 141 } 142 143 // TestArch returns the architecture under test. Pass this as GOARCH when 144 // building Go programs to be run in the QEMU environment. 145 func TestArch() string { 146 if env := os.Getenv("UROOT_TESTARCH"); env != "" { 147 return env 148 } 149 return "amd64" 150 } 151 152 // SkipWithoutQEMU skips the test when the QEMU environment variables are not 153 // set. This is already called by QEMUTest(), so use if some expensive 154 // operations are performed before calling QEMUTest(). 155 func SkipWithoutQEMU(t *testing.T) { 156 if _, ok := os.LookupEnv("UROOT_QEMU"); !ok { 157 t.Skip("QEMU test is skipped unless UROOT_QEMU is set") 158 } 159 if _, ok := os.LookupEnv("UROOT_KERNEL"); !ok { 160 t.Skip("QEMU test is skipped unless UROOT_KERNEL is set") 161 } 162 } 163 164 func saveCoverage(t *testing.T, path string) error { 165 // Coverage may not have been collected, for example if the kernel is 166 // not built with CONFIG_GCOV_KERNEL. 167 if fi, err := os.Stat(path); os.IsNotExist(err) || (err != nil && !fi.Mode().IsRegular()) { 168 return nil 169 } 170 171 // Move coverage to common directory. 172 uniqueCoveragePath := filepath.Join(coveragePath, t.Name(), fmt.Sprintf("%d", instance[t.Name()])) 173 instance[t.Name()]++ 174 if err := os.MkdirAll(uniqueCoveragePath, 0o770); err != nil { 175 return err 176 } 177 if err := os.Rename(path, filepath.Join(uniqueCoveragePath, filepath.Base(path))); err != nil { 178 return err 179 } 180 return nil 181 } 182 183 func QEMUTest(t *testing.T, o *Options) (*qemu.VM, func()) { 184 SkipWithoutQEMU(t) 185 186 // Delete any previous coverage data. 187 if _, ok := instance[t.Name()]; !ok { 188 testCoveragePath := filepath.Join(coveragePath, t.Name()) 189 if err := os.RemoveAll(testCoveragePath); err != nil && !os.IsNotExist(err) { 190 t.Logf("Error erasing previous coverage: %v", err) 191 } 192 } 193 194 if len(o.Name) == 0 { 195 o.Name = callerName(2) 196 } 197 if o.Logger == nil { 198 o.Logger = &ulogtest.Logger{TB: t} 199 } 200 if o.QEMUOpts.SerialOutput == nil { 201 o.QEMUOpts.SerialOutput = TestLineWriter(t, "serial") 202 } 203 204 // Create or reuse a temporary directory. This is exposed to the VM. 205 if o.TmpDir == "" { 206 tmpDir, err := os.MkdirTemp("", "uroot-integration") 207 if err != nil { 208 t.Fatalf("Failed to create temp dir: %v", err) 209 } 210 o.TmpDir = tmpDir 211 } 212 213 qOpts, err := QEMU(o) 214 if err != nil { 215 t.Fatalf("Failed to create QEMU VM %s: %v", o.Name, err) 216 } 217 218 vm, err := qOpts.Start() 219 if err != nil { 220 t.Fatalf("Failed to start QEMU VM %s: %v", o.Name, err) 221 } 222 223 return vm, func() { 224 vm.Close() 225 if !o.NoKernelCoverage { 226 if err := saveCoverage(t, filepath.Join(o.TmpDir, "kernel_coverage.tar")); err != nil { 227 t.Logf("Error saving kernel coverage: %v", err) 228 } 229 } 230 231 t.Logf("QEMU command line to reproduce %s:\n%s", o.Name, vm.CmdlineQuoted()) 232 if t.Failed() { 233 t.Log("Keeping temp dir: ", o.TmpDir) 234 } else if len(o.TmpDir) == 0 { 235 if err := os.RemoveAll(o.TmpDir); err != nil { 236 t.Logf("failed to remove temporary directory %s: %v", o.TmpDir, err) 237 } 238 } 239 } 240 } 241 242 // QEMU builds the u-root environment and prepares QEMU options given the test 243 // options and environment variables. 244 // 245 // QEMU will augment o.BuildOpts and o.QEMUOpts with configuration that the 246 // caller either requested (through the Options.Uinit field, for example) or 247 // that the caller did not set. 248 // 249 // QEMU returns the QEMU launch options or an error. 250 func QEMU(o *Options) (*qemu.Options, error) { 251 if len(o.Name) == 0 { 252 o.Name = callerName(2) 253 } 254 255 // Generate Elvish shell script of test commands in o.TmpDir. 256 if len(o.TestCmds) > 0 { 257 testFile := filepath.Join(o.TmpDir, "test.elv") 258 259 if err := os.WriteFile( 260 testFile, []byte(strings.Join(o.TestCmds, "\n")), 0o777); err != nil { 261 return nil, err 262 } 263 } 264 265 // Set the initramfs. 266 if len(o.QEMUOpts.Initramfs) == 0 { 267 o.QEMUOpts.Initramfs = filepath.Join(o.TmpDir, "initramfs.cpio") 268 if err := ChooseTestInitramfs(o.BuildOpts, o.Uinit, o.QEMUOpts.Initramfs); err != nil { 269 return nil, err 270 } 271 } 272 273 if len(o.QEMUOpts.Kernel) == 0 { 274 // Copy kernel to o.TmpDir for tests involving kexec. 275 kernel := filepath.Join(o.TmpDir, "kernel") 276 if err := cp.Copy(os.Getenv("UROOT_KERNEL"), kernel); err != nil { 277 return nil, err 278 } 279 o.QEMUOpts.Kernel = kernel 280 } 281 282 switch TestArch() { 283 case "amd64": 284 o.QEMUOpts.KernelArgs += " console=ttyS0 earlyprintk=ttyS0" 285 case "arm": 286 o.QEMUOpts.KernelArgs += " console=ttyAMA0" 287 } 288 o.QEMUOpts.KernelArgs += " uroot.vmtest" 289 290 var dir qemu.Device 291 if o.UseVVFAT { 292 dir = qemu.ReadOnlyDirectory{Dir: o.TmpDir} 293 } else { 294 dir = qemu.P9Directory{Dir: o.TmpDir, Arch: TestArch()} 295 } 296 o.QEMUOpts.Devices = append(o.QEMUOpts.Devices, qemu.VirtioRandom{}, dir) 297 298 if o.NoKernelCoverage { 299 o.QEMUOpts.KernelArgs += " UROOT_NO_KERNEL_COVERAGE=1" 300 } 301 302 return &o.QEMUOpts, nil 303 } 304 305 // ChooseTestInitramfs chooses which initramfs will be used for a given test and 306 // places it at the location given by outputFile. 307 // Default to the override initramfs if one is specified in the UROOT_INITRAMFS 308 // environment variable. Else, build an initramfs with the given parameters. 309 // If no uinit was provided, the generic one is used. 310 func ChooseTestInitramfs(o uroot.Opts, uinit, outputFile string) error { 311 override := os.Getenv("UROOT_INITRAMFS") 312 if len(override) > 0 { 313 log.Printf("Overriding with initramfs %q", override) 314 return cp.Copy(override, outputFile) 315 } 316 317 if len(uinit) == 0 { 318 log.Printf("Defaulting to generic initramfs") 319 uinit = "github.com/mvdan/u-root-coreutils/integration/testcmd/generic/uinit" 320 } 321 322 _, err := CreateTestInitramfs(o, uinit, outputFile) 323 return err 324 } 325 326 // CreateTestInitramfs creates an initramfs with the given build options and 327 // uinit, and writes it to the given output file. If no output file is provided, 328 // one will be created. 329 // The output file name is returned. It is the caller's responsibility to remove 330 // the initramfs file after use. 331 func CreateTestInitramfs(o uroot.Opts, uinit, outputFile string) (string, error) { 332 if o.Env == nil { 333 env := gbbgolang.Default() 334 env.CgoEnabled = false 335 env.GOARCH = TestArch() 336 o.Env = &env 337 } 338 339 if o.UrootSource == "" { 340 sourcePath, ok := os.LookupEnv("UROOT_SOURCE") 341 if !ok { 342 return "", fmt.Errorf("failed to get u-root source directory, please set UROOT_SOURCE to the absolute path of the u-root source directory") 343 } 344 o.UrootSource = sourcePath 345 } 346 347 logger := log.New(os.Stderr, "", 0) 348 349 // If build opts don't specify any commands, include all commands. Else, 350 // always add init and elvish. 351 var cmds []string 352 if len(o.Commands) == 0 { 353 cmds = []string{ 354 "github.com/mvdan/u-root-coreutils/cmds/core/*", 355 "github.com/mvdan/u-root-coreutils/cmds/exp/*", 356 } 357 } 358 359 if len(uinit) != 0 { 360 cmds = append(cmds, uinit) 361 } 362 363 // Add our commands to the build opts. 364 o.AddBusyBoxCommands(cmds...) 365 366 // Fill in the default build options if not specified. 367 if o.BaseArchive == nil { 368 o.BaseArchive = uroot.DefaultRamfs().Reader() 369 } 370 if len(o.InitCmd) == 0 { 371 o.InitCmd = "init" 372 } 373 if len(o.DefaultShell) == 0 { 374 o.DefaultShell = "elvish" 375 } 376 if len(o.TempDir) == 0 { 377 tempDir, err := os.MkdirTemp("", "initramfs-tempdir") 378 if err != nil { 379 return "", fmt.Errorf("Failed to create temp dir: %v", err) 380 } 381 defer os.RemoveAll(tempDir) 382 o.TempDir = tempDir 383 } 384 385 // Create an output file if one was not provided. 386 if len(outputFile) == 0 { 387 f, err := os.CreateTemp("", "initramfs.cpio") 388 if err != nil { 389 return "", fmt.Errorf("failed to create output file: %v", err) 390 } 391 outputFile = f.Name() 392 } 393 w, err := initramfs.CPIO.OpenWriter(logger, outputFile) 394 if err != nil { 395 return "", fmt.Errorf("Failed to create initramfs writer: %v", err) 396 } 397 o.OutputFile = w 398 399 return outputFile, uroot.CreateInitramfs(logger, o) 400 }