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 }