github.com/mvdan/u-root-coreutils@v0.0.0-20230122170626-c2eef2898555/pkg/vmtest/gotest.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  	"os"
    11  	"path"
    12  	"path/filepath"
    13  	"testing"
    14  
    15  	gbbgolang "github.com/u-root/gobusybox/src/pkg/golang"
    16  	"github.com/mvdan/u-root-coreutils/pkg/uio"
    17  	"github.com/mvdan/u-root-coreutils/pkg/uroot"
    18  	"github.com/mvdan/u-root-coreutils/pkg/vmtest/internal/json2test"
    19  	"golang.org/x/tools/go/packages"
    20  )
    21  
    22  func lookupPkgs(env gbbgolang.Environ, dir string, patterns ...string) ([]*packages.Package, error) {
    23  	cfg := &packages.Config{
    24  		Mode:  packages.NeedName | packages.NeedFiles,
    25  		Env:   append(os.Environ(), env.Env()...),
    26  		Dir:   dir,
    27  		Tests: true,
    28  	}
    29  	return packages.Load(cfg, patterns...)
    30  }
    31  
    32  // GolangTest compiles the unit tests found in pkgs and runs them in a QEMU VM.
    33  func GolangTest(t *testing.T, pkgs []string, o *Options) {
    34  	SkipWithoutQEMU(t)
    35  	// TODO: support arm
    36  	if TestArch() != "amd64" && TestArch() != "arm64" {
    37  		t.Skipf("test not supported on %s", TestArch())
    38  	}
    39  
    40  	vmCoverProfile, ok := os.LookupEnv("UROOT_QEMU_COVERPROFILE")
    41  	if !ok {
    42  		t.Log("QEMU test coverage is not collected unless UROOT_QEMU_COVERPROFILE is set")
    43  	}
    44  
    45  	if o == nil {
    46  		o = &Options{}
    47  	}
    48  
    49  	// Create a temporary directory.
    50  	if len(o.TmpDir) == 0 {
    51  		tmpDir, err := os.MkdirTemp("", "uroot-integration")
    52  		if err != nil {
    53  			t.Fatal(err)
    54  		}
    55  		o.TmpDir = tmpDir
    56  	}
    57  
    58  	if o.BuildOpts.UrootSource == "" {
    59  		sourcePath, ok := os.LookupEnv("UROOT_SOURCE")
    60  		if !ok {
    61  			t.Fatal("This test needs UROOT_SOURCE set to the absolute path of the checked out u-root source")
    62  		}
    63  		o.BuildOpts.UrootSource = sourcePath
    64  	}
    65  
    66  	// Set up u-root build options.
    67  	env := gbbgolang.Default()
    68  	env.CgoEnabled = false
    69  	env.GOARCH = TestArch()
    70  	o.BuildOpts.Env = &env
    71  
    72  	// Statically build tests and add them to the temporary directory.
    73  	var tests []string
    74  	testDir := filepath.Join(o.TmpDir, "tests")
    75  
    76  	if len(vmCoverProfile) > 0 {
    77  		f, err := os.Create(filepath.Join(o.TmpDir, "coverage.profile"))
    78  		if err != nil {
    79  			t.Fatalf("Could not create coverage file %v", err)
    80  		}
    81  		if err := f.Close(); err != nil {
    82  			t.Fatalf("Could not close coverage.profile: %v", err)
    83  		}
    84  	}
    85  
    86  	for _, pkg := range pkgs {
    87  		pkgDir := filepath.Join(testDir, pkg)
    88  		if err := os.MkdirAll(pkgDir, 0o755); err != nil {
    89  			t.Fatal(err)
    90  		}
    91  
    92  		testFile := filepath.Join(pkgDir, fmt.Sprintf("%s.test", path.Base(pkg)))
    93  
    94  		args := []string{
    95  			"test",
    96  			"-gcflags=all=-l",
    97  			"-ldflags", "-s -w",
    98  			"-c", pkg,
    99  			"-o", testFile,
   100  		}
   101  		if len(vmCoverProfile) > 0 {
   102  			args = append(args, "-covermode=atomic")
   103  		}
   104  
   105  		cmd := env.GoCmd(args...)
   106  		if stderr, err := cmd.CombinedOutput(); err != nil {
   107  			t.Fatalf("could not build %s: %v\n%s", pkg, err, string(stderr))
   108  		}
   109  
   110  		// When a package does not contain any tests, the test
   111  		// executable is not generated, so it is not included in the
   112  		// `tests` list.
   113  		if _, err := os.Stat(testFile); !os.IsNotExist(err) {
   114  			tests = append(tests, pkg)
   115  
   116  			pkgs, err := lookupPkgs(*o.BuildOpts.Env, "", pkg)
   117  			if err != nil {
   118  				t.Fatalf("Failed to look up package %q: %v", pkg, err)
   119  			}
   120  
   121  			// One directory = one package in standard Go, so
   122  			// finding the first file's parent directory should
   123  			// find us the package directory.
   124  			var dir string
   125  			for _, p := range pkgs {
   126  				if len(p.GoFiles) > 0 {
   127  					dir = filepath.Dir(p.GoFiles[0])
   128  				}
   129  			}
   130  			if dir == "" {
   131  				t.Fatalf("Could not find package directory for %q", pkg)
   132  			}
   133  
   134  			// Optimistically copy any files in the pkg's
   135  			// directory, in case e.g. a testdata dir is there.
   136  			if err := copyRelativeFiles(dir, filepath.Join(testDir, pkg)); err != nil {
   137  				t.Fatal(err)
   138  			}
   139  		}
   140  	}
   141  
   142  	// Create the CPIO and start QEMU.
   143  	o.BuildOpts.AddBusyBoxCommands("github.com/mvdan/u-root-coreutils/cmds/core/*")
   144  	o.BuildOpts.AddCommands(uroot.BinaryCmds("cmd/test2json")...)
   145  
   146  	// Specify the custom gotest uinit.
   147  	o.Uinit = "github.com/mvdan/u-root-coreutils/integration/testcmd/gotest/uinit"
   148  
   149  	tc := json2test.NewTestCollector()
   150  	serial := []io.Writer{
   151  		// Collect JSON test events in tc.
   152  		json2test.EventParser(tc),
   153  		// Write non-JSON output to log.
   154  		JSONLessTestLineWriter(t, "serial"),
   155  	}
   156  	if o.QEMUOpts.SerialOutput != nil {
   157  		serial = append(serial, o.QEMUOpts.SerialOutput)
   158  	}
   159  	o.QEMUOpts.SerialOutput = uio.MultiWriteCloser(serial...)
   160  	if len(vmCoverProfile) > 0 {
   161  		o.QEMUOpts.KernelArgs += " uroot.uinitargs=-coverprofile=/testdata/coverage.profile"
   162  	}
   163  
   164  	q, cleanup := QEMUTest(t, o)
   165  	defer cleanup()
   166  
   167  	if err := q.Expect("TESTS PASSED MARKER"); err != nil {
   168  		t.Errorf("Waiting for 'TESTS PASSED MARKER' signal: %v", err)
   169  	}
   170  
   171  	if len(vmCoverProfile) > 0 {
   172  		cov, err := os.Open(filepath.Join(o.TmpDir, "coverage.profile"))
   173  		if err != nil {
   174  			t.Fatalf("No coverage file shared from VM: %v", err)
   175  		}
   176  
   177  		out, err := os.OpenFile(vmCoverProfile, os.O_CREATE|os.O_RDWR|os.O_APPEND, 0644)
   178  		if err != nil {
   179  			t.Fatalf("Could not open vmcoverageprofile: %v", err)
   180  		}
   181  
   182  		if _, err := io.Copy(out, cov); err != nil {
   183  			t.Fatalf("Error copying coverage: %s", err)
   184  		}
   185  		if err := out.Close(); err != nil {
   186  			t.Fatalf("Could not close vmcoverageprofile: %v", err)
   187  		}
   188  		if err := cov.Close(); err != nil {
   189  			t.Fatalf("Could not close coverage.profile: %v", err)
   190  		}
   191  	}
   192  
   193  	// TODO: check that tc.Tests == tests
   194  	for pkg, test := range tc.Tests {
   195  		switch test.State {
   196  		case json2test.StateFail:
   197  			t.Errorf("Test %v failed:\n%v", pkg, test.FullOutput)
   198  		case json2test.StateSkip:
   199  			t.Logf("Test %v skipped", pkg)
   200  		case json2test.StatePass:
   201  			// Nothing.
   202  		default:
   203  			t.Errorf("Test %v left in state %v:\n%v", pkg, test.State, test.FullOutput)
   204  		}
   205  	}
   206  }
   207  
   208  func copyRelativeFiles(src string, dst string) error {
   209  	return filepath.Walk(src, func(path string, fi os.FileInfo, err error) error {
   210  		if err != nil {
   211  			return err
   212  		}
   213  
   214  		rel, err := filepath.Rel(src, path)
   215  		if err != nil {
   216  			return err
   217  		}
   218  
   219  		if fi.Mode().IsDir() {
   220  			return os.MkdirAll(filepath.Join(dst, rel), fi.Mode().Perm())
   221  		} else if fi.Mode().IsRegular() {
   222  			srcf, err := os.Open(path)
   223  			if err != nil {
   224  				return err
   225  			}
   226  			defer srcf.Close()
   227  			dstf, err := os.Create(filepath.Join(dst, rel))
   228  			if err != nil {
   229  				return err
   230  			}
   231  			defer dstf.Close()
   232  			_, err = io.Copy(dstf, srcf)
   233  			return err
   234  		}
   235  		return nil
   236  	})
   237  }