github.com/dylandreimerink/gobpfld@v0.6.1-0.20220205171531-e79c330ad608/cmd/testsuite/main.go (about) 1 package main 2 3 import ( 4 "bytes" 5 "crypto/sha256" 6 "encoding/hex" 7 "errors" 8 "fmt" 9 "io" 10 "io/fs" 11 "net/http" 12 "os" 13 "os/exec" 14 "os/user" 15 "path" 16 "sort" 17 "strconv" 18 "strings" 19 "time" 20 21 "github.com/dylandreimerink/gocovmerge" 22 "github.com/dylandreimerink/tarp" 23 "github.com/spf13/cobra" 24 "golang.org/x/sys/unix" 25 "golang.org/x/tools/cover" 26 ) 27 28 func main() { 29 //nolint:errcheck // can't do anything about an error, cobra prints it already 30 rootCmd().Execute() 31 } 32 33 func rootCmd() *cobra.Command { 34 c := &cobra.Command{} 35 36 c.AddCommand( 37 testCmd(), 38 ) 39 40 return c 41 } 42 43 var ( 44 flagVerbose bool 45 flagKeepTmp bool 46 flagNoPowerOff bool 47 48 flagOutputDir string 49 flagCover bool 50 flagCoverMode string 51 flagHTMLReport bool 52 53 flagShort bool 54 flagFailFast bool 55 flagRun string 56 flagTestEnvs []string 57 58 originalUID = func() int { 59 if os.Getenv("SUDO_UID") != "" { 60 uid, err := strconv.Atoi(os.Getenv("SUDO_UID")) 61 if err == nil { 62 return uid 63 } 64 } 65 return os.Getuid() 66 }() 67 originalGID = func() int { 68 if os.Getenv("SUDO_GID") != "" { 69 gid, err := strconv.Atoi(os.Getenv("SUDO_GID")) 70 if err == nil { 71 return gid 72 } 73 } 74 return os.Getgid() 75 }() 76 ) 77 78 func testCmd() *cobra.Command { 79 c := &cobra.Command{ 80 Use: "test", 81 Short: "Build and run unit/integration tests", 82 RunE: buildAndRunTests, 83 } 84 85 f := c.Flags() 86 f.BoolVarP(&flagVerbose, "verbose", "v", false, "If set, both this command will output verbosely and all called "+ 87 "commands will be called verbosely as well, thus outputting extra information") 88 f.BoolVar(&flagKeepTmp, "keep-tmp", false, "If set, the temporary directories will not be deleted after the test"+ 89 "run so intermediate files can be inspected") 90 f.BoolVar(&flagNoPowerOff, "no-poweroff", false, "If set, the VM will not power off automatically, allowing manual"+ 91 " inspection") 92 93 f.StringVarP(&flagOutputDir, "output-dir", "o", "./gobpfld-test-results", "Path to the directory where the result "+ 94 "files are stored (report, coverage, profiling, tracing)") 95 f.BoolVar(&flagCover, "cover", false, "Enable coverage analysis") 96 f.StringVar(&flagCoverMode, "covermode", "set", "set,count,atomic. Set the mode for coverage analysis for the"+ 97 " package[s] being tested. The default is \"set\" unless -race is enabled, in which case it is \"atomic\".") 98 f.BoolVar(&flagHTMLReport, "html-report", false, "If set, a HTML report will be created combining all available "+ 99 "data, including all results, coverage, and profiling") 100 101 f.BoolVar(&flagShort, "short", false, "Tell long-running tests to shorten their run time.") 102 f.BoolVar(&flagFailFast, "failfast", false, "Do not start new tests after the first test failure.") 103 f.StringVar(&flagRun, "run", "", "Run only those tests and examples matching the regular expression."+ 104 "For tests, the regular expression is split by unbracketed slash (/) "+ 105 "characters into a sequence of regular expressions, and each part "+ 106 "of a test's identifier must match the corresponding element in "+ 107 "the sequence, if any. Note that possible parents of matches are "+ 108 "run too, so that -run=X/Y matches and runs and reports the result "+ 109 "of all tests matching X, even those without sub-tests matching Y, "+ 110 "because it must run them to look for those sub-tests.") 111 f.StringArrayVar(&flagTestEnvs, "env", nil, "If set, tests will only be ran in the given environments") 112 return c 113 } 114 115 // A list of packages to be included in the test suite 116 var packages = []string{ 117 "github.com/dylandreimerink/gobpfld", 118 "github.com/dylandreimerink/gobpfld/bpfsys", 119 "github.com/dylandreimerink/gobpfld/bpftypes", 120 "github.com/dylandreimerink/gobpfld/ebpf", 121 "github.com/dylandreimerink/gobpfld/kernelsupport", 122 "github.com/dylandreimerink/gobpfld/perf", 123 "github.com/dylandreimerink/gobpfld/internal/cstr", 124 "github.com/dylandreimerink/gobpfld/internal/syscall", 125 126 // this package may contain complex tests which have no other logical place. 127 "github.com/dylandreimerink/gobpfld/cmd/testsuite/integration", 128 } 129 130 func printlnVerbose(args ...interface{}) { 131 if !flagVerbose { 132 return 133 } 134 135 fmt.Println(args...) 136 } 137 138 // testEnv represents a combination of factors to test for 139 type testEnv struct { 140 arch string 141 kernel string 142 bzImageURL string 143 } 144 145 var availableEnvs = map[string]testEnv{ 146 "linux-5.15.5-amd64": { 147 arch: "amd64", 148 kernel: "5.15.5", 149 bzImageURL: "https://github.com/dylandreimerink/bpfci/raw/master/dist/amd64-5.15.5-bzImage", 150 }, 151 "linux-5.4.167-amd64": { 152 arch: "amd64", 153 kernel: "5.4.167", 154 bzImageURL: "https://github.com/dylandreimerink/bpfci/raw/master/dist/amd64-5.4.167-bzImage", 155 }, 156 "linux-4.14.260-amd64": { 157 arch: "amd64", 158 kernel: "4.14.260", 159 bzImageURL: "https://github.com/dylandreimerink/bpfci/raw/master/dist/amd64-4.14.260-bzImage", 160 }, 161 // "linux-5.15.5-arm64": { 162 // arch: "arm64", 163 // kernel: "5.15.5", 164 // }, 165 } 166 167 func buildAndRunTests(cmd *cobra.Command, args []string) error { 168 cmd.SilenceUsage = true 169 170 // We need to run as root for a lot of the steps involved like mounting disks 171 err := elevate() 172 if err != nil { 173 return fmt.Errorf("error while elevating: %w", err) 174 } 175 176 environments := flagTestEnvs 177 if len(environments) == 0 { 178 for env := range availableEnvs { 179 environments = append(environments, env) 180 } 181 } else { 182 for _, env := range flagTestEnvs { 183 if _, ok := availableEnvs[env]; !ok { 184 var actualEnvNames []string 185 for ae := range availableEnvs { 186 actualEnvNames = append(actualEnvNames, ae) 187 } 188 189 return fmt.Errorf( 190 "'%s' is not a valid test environment, pick from: %s", 191 env, 192 strings.Join(actualEnvNames, ", "), 193 ) 194 } 195 } 196 } 197 sort.Strings(environments) 198 199 results := make(map[string]map[string]testResult) 200 for _, pkg := range packages { 201 // Test* to exclude benchmarks(for now) 202 testsStr, err := execCmd("go", "test", pkg, "-tags", "bpftests", "-list", "Test*") 203 if err != nil { 204 return fmt.Errorf("listing tests: %w", err) 205 } 206 207 lines := strings.Split(string(testsStr), "\n") 208 if len(lines) > 2 { 209 lines = lines[:len(lines)-2] 210 } else { 211 lines = nil 212 } 213 214 for _, env := range environments { 215 testMap := results[env] 216 if testMap == nil { 217 testMap = make(map[string]testResult) 218 } 219 220 for _, test := range lines { 221 testMap[test] = testResult{ 222 Name: test, 223 Status: statusUntested, 224 } 225 } 226 results[env] = testMap 227 } 228 } 229 230 // TODO run tests in goroutine and display progress bar (in non-verbose mode) 231 232 envFailed := make(map[string]bool) 233 234 for _, curEnvName := range environments { 235 envResults, err := testEnvironment(&testCtx{ 236 envName: curEnvName, 237 }) 238 for k, v := range envResults { 239 results[curEnvName][k] = v 240 } 241 if err != nil { 242 if !errors.Is(err, errTestsFailed) { 243 return err 244 } 245 246 envFailed[curEnvName] = true 247 248 // If we want to fail fast, don't test any other environments, report what we have 249 if flagFailFast { 250 break 251 } 252 } 253 } 254 255 if flagHTMLReport { 256 htmlPath := path.Join(flagOutputDir, "report.html") 257 printlnVerbose("OPEN:", htmlPath) 258 htmlFile, err := os.Create(htmlPath) 259 if err != nil { 260 return fmt.Errorf("create html report: %w", err) 261 } 262 defer htmlFile.Close() 263 defer func() { 264 err := os.Chown(htmlPath, originalUID, originalGID) 265 if err != nil { 266 fmt.Fprintln(os.Stderr, "CHOWN: err", err) 267 } 268 }() 269 270 err = renderHTMLReport(results, htmlFile) 271 if err != nil { 272 return fmt.Errorf("render html report: %w", err) 273 } 274 } else { 275 for env, envResults := range results { 276 if envFailed[env] { 277 fmt.Println("FAIL:", env) 278 } else { 279 fmt.Println("PASS:", env) 280 } 281 282 for _, testResult := range envResults { 283 fmt.Printf(" %s: %s (%s)\n", testResult.Status, testResult.Name, testResult.Duration) 284 } 285 } 286 } 287 288 // If there is at least one failed environment, return a non-0 exit code 289 if len(envFailed) != 0 { 290 os.Exit(2) 291 } 292 293 return nil 294 } 295 296 var errTestsFailed = errors.New("one or more tests failed") 297 298 type testCtx struct { 299 // Set before testEnvironment 300 envName string 301 302 // Set by testEnvironment 303 results map[string]testResult 304 curEnv testEnv 305 tmpDir string 306 executables []string 307 308 // Set by buildVMDiskImg 309 diskPath string 310 311 // Set by downloadLinux 312 bzPath string 313 initrdPath string 314 } 315 316 func testEnvironment(ctx *testCtx) (map[string]testResult, error) { 317 ctx.curEnv = availableEnvs[ctx.envName] 318 ctx.results = make(map[string]testResult) 319 320 printlnVerbose("=== Running tests for", ctx.envName, "===") 321 322 // example: /tmp/bpftestsuite-amd64-1099045701 323 var err error 324 ctx.tmpDir, err = os.MkdirTemp(os.TempDir(), strings.Join([]string{"bpftestsuite", ctx.curEnv.arch, "*"}, "-")) 325 if err != nil { 326 return ctx.results, fmt.Errorf("error while making a temporary directory: %w", err) 327 } 328 329 printlnVerbose("Using tempdir:", ctx.tmpDir) 330 331 // cleanup the temp dir after we are done, unless the user wan't to keep it 332 if !flagKeepTmp { 333 defer func() { 334 printlnVerbose("--- Cleaning up tmp dir ---") 335 err := os.RemoveAll(ctx.tmpDir) 336 if err != nil { 337 fmt.Fprintf(os.Stderr, "error while cleaning up tmp dir '%s': %s", ctx.tmpDir, err.Error()) 338 } 339 printlnVerbose("RM:", ctx.tmpDir) 340 }() 341 } 342 343 err = buildTestBinaries(ctx) 344 if err != nil { 345 return ctx.results, fmt.Errorf("buildTestBinaries: %w", err) 346 } 347 348 err = genVMRunScript(ctx) 349 if err != nil { 350 return ctx.results, fmt.Errorf("genVMRunScript: %w", err) 351 } 352 353 err = buildVMDiskImg(ctx) 354 if err != nil { 355 return ctx.results, fmt.Errorf("buildVMDiskImg: %w", err) 356 } 357 358 err = downloadLinux(ctx) 359 if err != nil { 360 return ctx.results, fmt.Errorf("downloadLinux: %w", err) 361 } 362 363 err = runTestInVM(ctx) 364 if err != nil { 365 return ctx.results, fmt.Errorf("runTestInVM: %w", err) 366 } 367 368 err = extractData(ctx) 369 if err != nil { 370 return ctx.results, fmt.Errorf("extractData: %w", err) 371 } 372 373 err = processResults(ctx) 374 if err != nil { 375 return ctx.results, fmt.Errorf("processResults: %w", err) 376 } 377 378 return ctx.results, nil 379 } 380 381 func buildTestBinaries(ctx *testCtx) error { 382 printlnVerbose("--- Build test binaries ---") 383 384 envVars := append( 385 os.Environ(), // Append to existing ENV vars 386 "GOARCH="+ctx.curEnv.arch, // Set target architecture 387 "CGO_ENABLED=0", // Disable CGO (to trigger static compilation) 388 ) 389 390 buildFlags := []string{ 391 "test", // invoke the test sub-command 392 "-c", // Compile the binary, but don't execute it 393 "-tags", "bpftests", // Include tests that use the BPF syscall 394 } 395 396 // Include cover mode when building, because according to `go help testflag` coverage reporting annotates 397 // the test binary. 398 if flagCover { 399 buildFlags = append(buildFlags, "-covermode", flagCoverMode) 400 buildFlags = append(buildFlags, "-coverpkg", strings.Join(packages, ",")) 401 } 402 403 if flagRun != "" { 404 buildFlags = append(buildFlags, "-run", flagRun) 405 } 406 407 ctx.executables = make([]string, 0, len(packages)) 408 for _, pkg := range packages { 409 pkgName := strings.Join([]string{path.Base(pkg), "test"}, ".") 410 execPath := path.Join(ctx.tmpDir, pkgName) 411 412 arguments := append( 413 buildFlags, 414 "-o", execPath, // Output test in the temporary directory 415 pkg, 416 ) 417 418 _, err := execEnvCmd(envVars, "go", arguments...) 419 if err != nil { 420 return fmt.Errorf("error while building tests: %w", err) 421 } 422 423 // If a package contains no tests, no executable is generated 424 if _, err := os.Stat(execPath); err == nil { 425 ctx.executables = append(ctx.executables, pkgName) 426 } 427 } 428 429 return nil 430 } 431 432 func genVMRunScript(ctx *testCtx) error { 433 printlnVerbose("--- Generate VM run script ---") 434 435 // Make a buffer for the actual script which will execute the tests inside the VM 436 scriptBuf := bytes.Buffer{} 437 scriptBuf.WriteString("#!/bin/sh\n\n") 438 439 // The the location where the disk will be mounted in the VM 440 const vmPath = "/mnt/root" 441 for _, execName := range ctx.executables { 442 flags := []string{ 443 // Always return verbose output, it contains info about which tests actually ran or were skipped 444 "-test.v", 445 } 446 447 if flagCover { 448 flags = append(flags, "-test.coverprofile", path.Join(vmPath, execName+".cover")) 449 } 450 451 if flagFailFast { 452 flags = append(flags, "-test.failfast") 453 } 454 455 if flagShort { 456 flags = append(flags, "-test.short") 457 } 458 459 if flagRun != "" { 460 flags = append(flags, "-test.run", flagRun) 461 } 462 463 // Run script, write stdout to "$exec.results", write stderr to "$exec.error" and the exit code to "$exec.exit" 464 fmt.Fprintf( 465 &scriptBuf, 466 "%s %s > %s 2> %s\necho $? > %s\n", 467 path.Join(vmPath, execName), 468 strings.Join(flags, " "), 469 path.Join(vmPath, execName+".results"), 470 path.Join(vmPath, execName+".error"), 471 path.Join(vmPath, execName+".exit"), 472 ) 473 } 474 475 if !flagNoPowerOff { 476 // The last command is the poweroff command(busybox shutdown command), this will cause the VM to exit 477 // after all tests have been ran. 478 fmt.Fprintln(&scriptBuf, "poweroff -f") 479 } 480 481 // Write the shell script 482 printlnVerbose(scriptBuf.String()) 483 484 //nolint:gosec // Creating an executable on purpose 485 err := os.WriteFile(path.Join(ctx.tmpDir, "run.sh"), scriptBuf.Bytes(), 0755) 486 if err != nil { 487 return fmt.Errorf("error while writing run script: %w", err) 488 } 489 490 return nil 491 } 492 493 const mntPath = "/mnt/bpftestdisk" 494 495 func buildVMDiskImg(ctx *testCtx) error { 496 printlnVerbose("--- Build VM disk image ---") 497 498 ctx.diskPath = path.Join(ctx.tmpDir, "disk.img") 499 // Create a 256MB(should be plenty) raw disk which we will later use to add the test executables to the VM 500 // and later get back the test results 501 _, err := execCmd("qemu-img", "create", ctx.diskPath, "256M") 502 if err != nil { 503 return fmt.Errorf("error while creating qemu image: %w", err) 504 } 505 506 // Add master boot record partition table to raw image 507 _, err = execCmd( 508 "parted", 509 "-s", ctx.diskPath, 510 "mklabel msdos", 511 "mkpart primary ext2 2048s 100%", 512 ) 513 if err != nil { 514 return fmt.Errorf("error while creating qemu image: %w", err) 515 } 516 517 // Create a loop device from the disk file which will allow us to mount it 518 loopDevBytes, err := execCmd("losetup", "--partscan", "--show", "--find", ctx.diskPath) 519 if err != nil { 520 return fmt.Errorf("error while creating loop device: %w", err) 521 } 522 loopDev := strings.TrimSpace(string(loopDevBytes)) 523 524 printlnVerbose("MKDIR: ", mntPath) 525 err = os.Mkdir(mntPath, 0755) 526 if err != nil && err != fs.ErrExist { 527 return fmt.Errorf("error while making mnt dir: %w", err) 528 } 529 defer func() { 530 // Remove the mount path 531 printlnVerbose("RM:", mntPath) 532 err = os.Remove(mntPath) 533 if err != nil { 534 fmt.Fprintf(os.Stderr, "Error while deleting mount dir '%s': %s", mntPath, err.Error()) 535 } 536 }() 537 538 // Make a EXT2 filesystem on the loop device's 1st partition 539 _, err = execCmd("mkfs", "-t", "ext2", "-L", "bpfdisk", loopDev+"p1") 540 if err != nil { 541 return fmt.Errorf("error while creating FS on loop device: %w", err) 542 } 543 544 // Mount the first partion of the loop device 545 _, err = execCmd("mount", loopDev+"p1", mntPath) 546 if err != nil { 547 return fmt.Errorf("error while mounting loop device: %w", err) 548 } 549 550 // Copy all executables and the run script to the new disk 551 copyFiles := append(ctx.executables, "run.sh") 552 for _, fileName := range copyFiles { 553 tmpPath := path.Join(ctx.tmpDir, fileName) 554 mntPath := path.Join(mntPath, fileName) 555 556 err = copyFile(tmpPath, mntPath, 0755) 557 if err != nil { 558 return err 559 } 560 } 561 562 // Unmount the loop device 563 _, err = execCmd("umount", mntPath) 564 if err != nil { 565 return fmt.Errorf("error while unmounting loop device: %w", err) 566 } 567 568 // Remove the loop device 569 _, err = execCmd("losetup", "-d", loopDev) 570 if err != nil { 571 fmt.Fprintf(os.Stderr, "Error while deleting loop device '%s': %s", loopDev, err.Error()) 572 } 573 574 return nil 575 } 576 577 func downloadLinux(ctx *testCtx) error { 578 printlnVerbose("--- Checking/downloading bzImage and initrd ---") 579 580 const cacheDir = "/var/cache/bpfld" 581 printlnVerbose("MKDIR", cacheDir) 582 err := os.MkdirAll(cacheDir, 0755) 583 if err != nil { 584 return fmt.Errorf("error while creating cache directory: %w", err) 585 } 586 587 bzFilename := fmt.Sprintf("%s-%s.bzImage", ctx.curEnv.arch, ctx.curEnv.kernel) 588 ctx.bzPath = path.Join(cacheDir, bzFilename) 589 dlBZ := false 590 591 printlnVerbose("CHECKSUM:", ctx.bzPath) 592 bzFile, err := os.Open(ctx.bzPath) 593 if err != nil { 594 dlBZ = true 595 } else { 596 // Calculate the sha256 hash of the existing bzImage 597 h := sha256.New() 598 599 _, err = io.Copy(h, bzFile) 600 if err != nil { 601 return fmt.Errorf("error while hashing bzImage: %w", err) 602 } 603 bzFile.Close() 604 605 bzHash := h.Sum(nil) 606 607 printlnVerbose("GET: ", ctx.curEnv.bzImageURL+".sha256") 608 resp, err := http.Get(ctx.curEnv.bzImageURL + ".sha256") 609 if err != nil { 610 return fmt.Errorf("error while downloading bzImage hash: %w", err) 611 } 612 defer resp.Body.Close() 613 614 body, err := io.ReadAll(resp.Body) 615 if err != nil { 616 return fmt.Errorf("error while reading bzImage hash: %w", err) 617 } 618 619 bodyStr := strings.TrimSpace(string(body)) 620 if bodyStr != hex.EncodeToString(bzHash) { 621 printlnVerbose( 622 "Remote =", bodyStr+",", 623 "Local =", hex.EncodeToString(bzHash), 624 ) 625 dlBZ = true 626 } 627 } 628 629 // If we can't stat the bzImage in the cache dir, download it 630 if dlBZ { 631 err = func() error { 632 printlnVerbose("DOWNLOAD: ", ctx.curEnv.bzImageURL) 633 resp, err := http.Get(ctx.curEnv.bzImageURL) 634 if err != nil { 635 return fmt.Errorf("error while downloading bzImage: %w", err) 636 } 637 defer resp.Body.Close() 638 639 bzFile, err = os.OpenFile(ctx.bzPath, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0644) 640 if err != nil { 641 return fmt.Errorf("error while creating bzImage: %w", err) 642 } 643 defer bzFile.Close() 644 645 _, err = io.Copy(bzFile, resp.Body) 646 if err != nil { 647 return fmt.Errorf("error while copying bzImage: %w", err) 648 } 649 650 return nil 651 }() 652 if err != nil { 653 return err 654 } 655 } 656 657 const initrdURL = "https://github.com/dylandreimerink/bpfci/raw/master/dist/amd64-initrd.gz" 658 ctx.initrdPath = path.Join(cacheDir, "initrd.gz") 659 dlInitrd := false 660 661 printlnVerbose("CHECKSUM:", ctx.initrdPath) 662 initrdFile, err := os.Open(ctx.initrdPath) 663 if err != nil { 664 dlInitrd = true 665 } else { 666 // Calculate the sha256 hash of the existing bzImage 667 h := sha256.New() 668 669 _, err = io.Copy(h, initrdFile) 670 if err != nil { 671 return fmt.Errorf("error while hashing initrd: %w", err) 672 } 673 bzFile.Close() 674 675 initrdHash := h.Sum(nil) 676 677 printlnVerbose("GET: ", initrdURL+".sha256") 678 resp, err := http.Get(initrdURL + ".sha256") 679 if err != nil { 680 return fmt.Errorf("error while downloading initrd hash: %w", err) 681 } 682 defer resp.Body.Close() 683 684 body, err := io.ReadAll(resp.Body) 685 if err != nil { 686 return fmt.Errorf("error while reading initrd hash: %w", err) 687 } 688 689 bodyStr := strings.TrimSpace(string(body)) 690 if bodyStr != hex.EncodeToString(initrdHash) { 691 printlnVerbose( 692 "Remote =", bodyStr+",", 693 "Local =", hex.EncodeToString(initrdHash), 694 ) 695 dlInitrd = true 696 } 697 } 698 699 if dlInitrd { 700 err = func() error { 701 printlnVerbose("DOWNLOAD: ", initrdURL) 702 resp, err := http.Get(initrdURL) 703 if err != nil { 704 return fmt.Errorf("error while downloading initrd: %w", err) 705 } 706 defer resp.Body.Close() 707 708 initrdFile, err = os.OpenFile(ctx.initrdPath, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0644) 709 if err != nil { 710 return fmt.Errorf("error while creating initrd: %w", err) 711 } 712 defer initrdFile.Close() 713 714 _, err = io.Copy(initrdFile, resp.Body) 715 if err != nil { 716 return fmt.Errorf("error while copying initrd: %w", err) 717 } 718 719 return nil 720 }() 721 if err != nil { 722 return err 723 } 724 } 725 726 return nil 727 } 728 729 func runTestInVM(ctx *testCtx) error { 730 // TODO setup bridge and tap devices 731 printlnVerbose("--- Starting test run in VM ---") 732 733 arguments := []string{ 734 "-m", "4G", // Give the VM 4GB RAM, should be plenty 735 "-kernel", ctx.bzPath, // Start kernel for the given environment 736 "-initrd", ctx.initrdPath, // Use this initial ram disk (which will call our run.sh after setup) 737 "-drive", "format=raw,file=" + ctx.diskPath, // Use the created disk as a drive 738 "-netdev", "tap,id=bpfnet0,ifname=bpfci-tap1,script=no,downscript=no", 739 "-device", "e1000,mac=de:ad:be:ef:00:01,netdev=bpfnet0", // Add a E1000 NIC 740 "-append", "root=/dev/sda1", 741 // TODO run with no-graphics and capture kernel output 742 } 743 _, err := execCmd("qemu-system-x86_64", arguments...) 744 if err != nil { 745 fmt.Fprintf(os.Stderr, "Error while starting VM: %s", err.Error()) 746 } 747 748 return nil 749 } 750 751 func extractData(ctx *testCtx) error { 752 printlnVerbose("--- Remounting disk ---") 753 754 // Create a loop device from the disk file which will allow us to mount it 755 loopDevBytes, err := execCmd("losetup", "--partscan", "--show", "--find", ctx.diskPath) 756 if err != nil { 757 return fmt.Errorf("error while creating loop device: %w", err) 758 } 759 loopDev := strings.TrimSpace(string(loopDevBytes)) 760 defer func() { 761 // Remove the loop device 762 _, err = execCmd("losetup", "-d", loopDev) 763 if err != nil { 764 fmt.Fprintf(os.Stderr, "Error while deleting loop device '%s': %s", loopDev, err.Error()) 765 } 766 }() 767 768 printlnVerbose("MKDIR: ", mntPath) 769 err = os.Mkdir(mntPath, 0755) 770 if err != nil && err != fs.ErrExist { 771 return fmt.Errorf("error while making mnt dir: %w", err) 772 } 773 defer func() { 774 // Remove the mount path 775 printlnVerbose("RM:", mntPath) 776 err = os.Remove(mntPath) 777 if err != nil { 778 fmt.Fprintf(os.Stderr, "Error while deleting mount dir '%s': %s", mntPath, err.Error()) 779 } 780 }() 781 782 // Mount the first partion of the loop device 783 _, err = execCmd("mount", loopDev+"p1", mntPath) 784 if err != nil { 785 return fmt.Errorf("error while mounting loop device: %w", err) 786 } 787 defer func() { 788 // Unmount the loop device 789 _, err = execCmd("umount", mntPath) 790 if err != nil { 791 fmt.Fprintf(os.Stderr, "Error while unmounting loop device: %s", err.Error()) 792 } 793 }() 794 795 copyFiles := []string{} 796 for _, execName := range ctx.executables { 797 copyFiles = append(copyFiles, execName+".results") 798 copyFiles = append(copyFiles, execName+".error") 799 copyFiles = append(copyFiles, execName+".exit") 800 801 if flagCover { 802 copyFiles = append(copyFiles, execName+".cover") 803 } 804 } 805 806 for _, fileName := range copyFiles { 807 err = copyFile(path.Join(mntPath, fileName), path.Join(ctx.tmpDir, fileName), 0644) 808 if err != nil { 809 fmt.Fprintln(os.Stderr, "error while copying:", err.Error()) 810 } 811 } 812 813 // Merge all .cover files 814 if flagCover { 815 var merged []*cover.Profile 816 for _, execName := range ctx.executables { 817 profiles, err := cover.ParseProfiles(path.Join(ctx.tmpDir, execName+".cover")) 818 if err != nil { 819 fmt.Fprintln(os.Stderr, "failed to parse profiles:", err.Error()) 820 continue 821 } 822 for _, p := range profiles { 823 merged = gocovmerge.AddProfile(merged, p) 824 } 825 } 826 827 coverPath := path.Join(ctx.tmpDir, "gobpfld.cover") 828 coverFile, err := os.Create(path.Join(ctx.tmpDir, "gobpfld.cover")) 829 if err != nil { 830 return fmt.Errorf("make combined coverfile: %w", err) 831 } 832 833 gocovmerge.DumpProfiles(merged, coverFile) 834 835 err = coverFile.Close() 836 if err != nil { 837 return fmt.Errorf("close combined coverfile: %w", err) 838 } 839 err = os.Chown(coverPath, originalUID, originalGID) 840 if err != nil { 841 return fmt.Errorf("chown combined coverfile: %w", err) 842 } 843 844 if flagHTMLReport { 845 err = tarp.GenerateHTMLReport([]string{coverPath}, path.Join(ctx.tmpDir, "gobpfld.cover.html")) 846 if err != nil { 847 return fmt.Errorf("make html coverage report: %w", err) 848 } 849 err = os.Chown(path.Join(ctx.tmpDir, "gobpfld.cover.html"), originalUID, originalGID) 850 if err != nil { 851 return fmt.Errorf("chown html coverage report: %w", err) 852 } 853 } 854 } 855 856 copyFiles = []string{} 857 if flagCover { 858 copyFiles = append(copyFiles, "gobpfld.cover") 859 if flagHTMLReport { 860 copyFiles = append(copyFiles, "gobpfld.cover.html") 861 } 862 } 863 864 // If there are no output files 865 if len(copyFiles) == 0 { 866 return nil 867 } 868 869 outDir := path.Join(flagOutputDir, ctx.envName) 870 871 printlnVerbose("STAT:", outDir) 872 // If the directory exists, remove it 873 if _, err = os.Stat(outDir); err == nil { 874 printlnVerbose("RM:", outDir) 875 os.RemoveAll(outDir) 876 } 877 878 printlnVerbose("MKDIR:", outDir) 879 err = os.MkdirAll(outDir, 0755) 880 if err != nil { 881 return fmt.Errorf("make output dir: %w", err) 882 } 883 884 printlnVerbose("CHOWN:", flagOutputDir) 885 err = os.Chown(flagOutputDir, originalUID, originalGID) 886 if err != nil { 887 return fmt.Errorf("chown output dir: %w", err) 888 } 889 890 printlnVerbose("CHOWN:", outDir) 891 err = os.Chown(outDir, originalUID, originalGID) 892 if err != nil { 893 return fmt.Errorf("chown output dir: %w", err) 894 } 895 896 for _, fileName := range copyFiles { 897 err = copyFile(path.Join(ctx.tmpDir, fileName), path.Join(outDir, fileName), 0644) 898 if err != nil { 899 return err 900 } 901 902 printlnVerbose("CHOWN:", path.Join(outDir, fileName)) 903 err = os.Chown(path.Join(outDir, fileName), originalUID, originalGID) 904 if err != nil { 905 return fmt.Errorf("chown output file: %w", err) 906 } 907 } 908 909 return nil 910 } 911 912 func processResults(ctx *testCtx) error { 913 printlnVerbose("--- Processing results ---") 914 915 exitWithErr := false 916 917 for _, execName := range ctx.executables { 918 exitPath := path.Join(ctx.tmpDir, execName+".exit") 919 errCodeBytes, err := os.ReadFile(exitPath) 920 if err != nil { 921 return fmt.Errorf("read exit code file '%s': %w", exitPath, err) 922 } 923 924 exitCode, err := strconv.Atoi(strings.TrimSpace(string(errCodeBytes))) 925 if err != nil { 926 return fmt.Errorf("exit code file atoi '%s': %w", exitPath, err) 927 } 928 929 resultPath := path.Join(ctx.tmpDir, execName+".results") 930 testResults, err := os.ReadFile(resultPath) 931 if err != nil { 932 return fmt.Errorf("read results file '%s': %w", resultPath, err) 933 } 934 935 // Get back the package name from the executable name 936 pkg := strings.TrimSuffix(execName, ".test") 937 938 // If error code == 0, the executable returned without errors 939 if exitCode != 0 { 940 printlnVerbose(fmt.Sprintf("%s FAIL\nTests exited with code '%d'", pkg, exitCode)) 941 exitWithErr = true 942 943 errorPath := path.Join(ctx.tmpDir, execName+".error") 944 testError, err := os.ReadFile(errorPath) 945 if err != nil { 946 return fmt.Errorf("read error file '%s': %w", errorPath, err) 947 } 948 949 fmt.Printf("Stdout:\n%s\n", string(testResults)) 950 fmt.Printf("Stderr:\n%s\n", string(testError)) 951 } 952 953 for _, line := range strings.Split(string(testResults), "\n") { 954 const ( 955 passPrefix = "--- PASS:" 956 failPrefix = "--- FAIL:" 957 skipPrefix = "--- SKIP:" 958 ) 959 960 var status testStatus 961 962 if strings.HasPrefix(line, passPrefix) { 963 line = strings.TrimSpace(strings.TrimPrefix(line, passPrefix)) 964 status = statusPass 965 } else if strings.HasPrefix(line, failPrefix) { 966 line = strings.TrimSpace(strings.TrimPrefix(line, failPrefix)) 967 status = statusFail 968 } else if strings.HasPrefix(line, skipPrefix) { 969 line = strings.TrimSpace(strings.TrimPrefix(line, skipPrefix)) 970 status = statusSkip 971 } else { 972 continue 973 } 974 975 parts := strings.Split(line, " ") 976 if len(parts) < 2 { 977 fmt.Fprintln(os.Stderr, "unexpected test results(parts < 2)") 978 continue 979 } 980 981 testName := parts[0] 982 durationStr := strings.Trim(parts[1], "()") 983 duration, err := time.ParseDuration(durationStr) 984 if err != nil { 985 fmt.Fprintln(os.Stderr, "unexpected test results:", err.Error()) 986 continue 987 } 988 989 ctx.results[testName] = testResult{ 990 Name: testName, 991 Status: status, 992 Duration: duration, 993 } 994 } 995 } 996 997 if exitWithErr { 998 return errTestsFailed 999 } 1000 1001 return nil 1002 } 1003 1004 type testStatus string 1005 1006 const ( 1007 // has not (yet) been run 1008 statusUntested testStatus = "UNTESTED" 1009 // tested and passed 1010 statusPass testStatus = "PASS" 1011 // tested and failed 1012 statusFail testStatus = "FAIL" 1013 // skipped testing, due to -short flag or kernel incompatibility 1014 statusSkip testStatus = "SKIP" 1015 ) 1016 1017 type testResult struct { 1018 Name string 1019 Status testStatus 1020 Duration time.Duration 1021 // TODO return sub-test data? 1022 } 1023 1024 func copyFile(from, to string, perm fs.FileMode) error { 1025 printlnVerbose("CP:", from, "->", to) 1026 fromFile, err := os.Open(from) 1027 if err != nil { 1028 return fmt.Errorf("error while opening file: %w", err) 1029 } 1030 1031 toFile, err := os.OpenFile(to, os.O_CREATE|os.O_WRONLY, perm) 1032 if err != nil { 1033 return fmt.Errorf("error while creating file: %w", err) 1034 } 1035 1036 _, err = io.Copy(toFile, fromFile) 1037 if err != nil { 1038 return fmt.Errorf("error while copying file: %w", err) 1039 } 1040 1041 err = toFile.Close() 1042 if err != nil { 1043 return fmt.Errorf("error while closing file: %w", err) 1044 } 1045 1046 err = fromFile.Close() 1047 if err != nil { 1048 return fmt.Errorf("error while closing file: %w", err) 1049 } 1050 1051 return nil 1052 } 1053 1054 func execCmd(name string, args ...string) ([]byte, error) { 1055 return execEnvCmd(nil, name, args...) 1056 } 1057 1058 func execEnvCmd(env []string, name string, args ...string) ([]byte, error) { 1059 // we have to do this bullshit because you can't explode a ...string to a ...interface{} 1060 // so we have to joint into as single string which can be passed to a ...interface{} 1061 printlnVerbose(strings.Join(append([]string{"EXEC:", name}, args...), " ")) 1062 1063 cmd := exec.Command(name, args...) 1064 if env != nil { 1065 cmd.Env = env 1066 } 1067 output, err := cmd.Output() 1068 if err != nil { 1069 fmt.Fprintln(os.Stderr, string(output)) 1070 if ee, ok := err.(*exec.ExitError); ok { 1071 fmt.Fprintln(os.Stderr, string(ee.Stderr)) 1072 } 1073 return nil, err 1074 } 1075 1076 return output, nil 1077 } 1078 1079 // elevate checks if we are currently running as root, if not we will request the user to elevate the program 1080 func elevate() error { 1081 curUser, err := user.Current() 1082 if err != nil { 1083 return fmt.Errorf("error while getting user: %w", err) 1084 } 1085 1086 // If we are user 0(root), we don't need to elevate 1087 if curUser.Uid == "0" { 1088 return nil 1089 } 1090 1091 fmt.Println("This testsuit requires root privileges, attempting to elevate via sudo...") 1092 1093 exec := lookupExec("sudo") 1094 if exec == "" { 1095 // Fallback if we can't resolve via PATH 1096 exec = "/usr/bin/sudo" 1097 } 1098 1099 // Elevate to root by execve'ing sudo with the current args. This should prompt the user for their sudo password 1100 // and then continue executing this program(again from the start, since this process will be replaced) 1101 // NOTE: The `--preserve-env=PATH` will make sure that the current PATH is preserved which is important since most 1102 // users will not have setup root with the correct go environment variables. 1103 err = unix.Exec(exec, append([]string{"sudo", "--preserve-env=PATH"}, os.Args...), os.Environ()) 1104 if err != nil { 1105 return fmt.Errorf("error execve'ing into sudo: %w", err) 1106 } 1107 1108 return nil 1109 } 1110 1111 // lookupExec performs a executable lookup based on the PATH environment variable 1112 func lookupExec(name string) string { 1113 pathVar := os.Getenv("PATH") 1114 exec := "" 1115 1116 for _, dir := range strings.Split(pathVar, ":") { 1117 abs := path.Join(dir, name) 1118 stat, err := os.Stat(abs) 1119 if err != nil || stat.IsDir() { 1120 continue 1121 } 1122 exec = abs 1123 break 1124 } 1125 1126 return exec 1127 }