gvisor/test/syscalls/syscall_test_runner.go

327 lines
9.5 KiB
Go

// Copyright 2018 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// Binary syscall_test_runner runs the syscall test suites in gVisor
// containers and on the host platform.
package main
import (
"flag"
"fmt"
"io/ioutil"
"math"
"os"
"os/exec"
"path/filepath"
"strconv"
"strings"
"syscall"
"testing"
specs "github.com/opencontainers/runtime-spec/specs-go"
"golang.org/x/sys/unix"
"gvisor.googlesource.com/gvisor/pkg/log"
"gvisor.googlesource.com/gvisor/runsc/specutils"
"gvisor.googlesource.com/gvisor/runsc/test/testutil"
"gvisor.googlesource.com/gvisor/test/syscalls/gtest"
)
// Location of syscall tests, relative to the repo root.
const testDir = "test/syscalls/linux"
var (
testName = flag.String("test-name", "", "name of test binary to run")
debug = flag.Bool("debug", false, "enable debug logs")
strace = flag.Bool("strace", false, "enable strace logs")
platform = flag.String("platform", "ptrace", "platform to run on")
useTmpfs = flag.Bool("use-tmpfs", false, "mounts tmpfs for /tmp")
fileAccess = flag.String("file-access", "exclusive", "mounts root in exclusive or shared mode")
parallel = flag.Bool("parallel", false, "run tests in parallel")
runscPath = flag.String("runsc", "", "path to runsc binary")
)
// runTestCaseNative runs the test case directly on the host machine.
func runTestCaseNative(testBin string, tc gtest.TestCase, t *testing.T) {
// These tests might be running in parallel, so make sure they have a
// unique test temp dir.
tmpDir, err := ioutil.TempDir(testutil.TmpDir(), "")
if err != nil {
t.Fatalf("could not create temp dir: %v", err)
}
defer os.RemoveAll(tmpDir)
// Replace TEST_TMPDIR in the current environment with something
// unique.
env := os.Environ()
newEnvVar := "TEST_TMPDIR=" + tmpDir
var found bool
for i, kv := range env {
if strings.HasPrefix(kv, "TEST_TMPDIR=") {
env[i] = newEnvVar
found = true
break
}
}
if !found {
env = append(env, newEnvVar)
}
// Remove env variables that cause the gunit binary to write output
// files, since they will stomp on eachother, and on the output files
// from this go test.
env = filterEnv(env, []string{"GUNIT_OUTPUT", "TEST_PREMATURE_EXIT_FILE", "XML_OUTPUT_FILE"})
// Remove shard env variables so that the gunit binary does not try to
// intepret them.
env = filterEnv(env, []string{"TEST_SHARD_INDEX", "TEST_TOTAL_SHARDS"})
cmd := exec.Command(testBin, gtest.FilterTestFlag+"="+tc.FullName())
cmd.Env = env
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
if err := cmd.Run(); err != nil {
ws := err.(*exec.ExitError).Sys().(syscall.WaitStatus)
t.Errorf("test %q exited with status %d, want 0", tc.FullName(), ws.ExitStatus())
}
}
// runsTestCaseRunsc runs the test case in runsc.
func runTestCaseRunsc(testBin string, tc gtest.TestCase, t *testing.T) {
rootDir, err := testutil.SetupRootDir()
if err != nil {
t.Fatalf("SetupRootDir failed: %v", err)
}
defer os.RemoveAll(rootDir)
// Run a new container with the test executable and filter for the
// given test suite and name.
spec := testutil.NewSpecWithArgs(testBin, gtest.FilterTestFlag+"="+tc.FullName())
// Mark the root as writeable, as some tests attempt to
// write to the rootfs, and expect EACCES, not EROFS.
spec.Root.Readonly = false
// Test spec comes with pre-defined mounts that we don't want. Reset it.
spec.Mounts = nil
if *useTmpfs {
// Forces '/tmp' to be mounted as tmpfs, otherwise test that rely on
// features available in gVisor's tmpfs and not gofers, may fail.
spec.Mounts = []specs.Mount{
{Destination: "/tmp", Type: "tmpfs"},
}
}
// Set environment variable that indicates we are
// running in gVisor and with the given platform.
platformVar := "TEST_ON_GVISOR"
env := append(os.Environ(), platformVar+"="+*platform)
// Remove env variables that cause the gunit binary to write output
// files, since they will stomp on eachother, and on the output files
// from this go test.
env = filterEnv(env, []string{"GUNIT_OUTPUT", "TEST_PREMATURE_EXIT_FILE", "XML_OUTPUT_FILE"})
// Remove shard env variables so that the gunit binary does not try to
// intepret them.
env = filterEnv(env, []string{"TEST_SHARD_INDEX", "TEST_TOTAL_SHARDS"})
// Set TEST_TMPDIR to /tmp, as some of the syscall tests require it to
// be backed by tmpfs.
for i, kv := range env {
if strings.HasPrefix(kv, "TEST_TMPDIR=") {
env[i] = "TEST_TMPDIR=/tmp"
break
}
}
spec.Process.Env = env
bundleDir, err := testutil.SetupBundleDir(spec)
if err != nil {
t.Fatalf("SetupBundleDir failed: %v", err)
}
defer os.RemoveAll(bundleDir)
id := testutil.UniqueContainerID()
log.Infof("Running test %q in container %q", tc.FullName(), id)
specutils.LogSpec(spec)
args := []string{
"-platform", *platform,
"-root", rootDir,
"-file-access", *fileAccess,
"--network=none",
"-log-format=text",
"-TESTONLY-unsafe-nonroot=true",
}
if *debug {
args = append(args, "-debug", "-log-packets=true")
}
if *strace {
args = append(args, "-strace")
}
if outDir, ok := syscall.Getenv("TEST_UNDECLARED_OUTPUTS_DIR"); ok {
args = append(args, "-debug-log", outDir+"/")
}
// Current process doesn't have CAP_SYS_ADMIN, create user namespace and run
// as root inside that namespace to get it.
args = append(args, "run", "--bundle", bundleDir, id)
cmd := exec.Command(*runscPath, args...)
cmd.SysProcAttr = &syscall.SysProcAttr{
Cloneflags: syscall.CLONE_NEWUSER | syscall.CLONE_NEWNS,
// Set current user/group as root inside the namespace.
UidMappings: []syscall.SysProcIDMap{
{ContainerID: 0, HostID: os.Getuid(), Size: 1},
},
GidMappings: []syscall.SysProcIDMap{
{ContainerID: 0, HostID: os.Getgid(), Size: 1},
},
GidMappingsEnableSetgroups: false,
Credential: &syscall.Credential{
Uid: 0,
Gid: 0,
},
}
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
if err = cmd.Run(); err != nil {
t.Errorf("test %q exited with status %v, want 0", tc.FullName(), err)
}
}
// filterEnv returns an environment with the blacklisted variables removed.
func filterEnv(env, blacklist []string) []string {
var out []string
for _, kv := range env {
ok := true
for _, k := range blacklist {
if strings.HasPrefix(kv, k+"=") {
ok = false
break
}
}
if ok {
out = append(out, kv)
}
}
return out
}
func fatalf(s string, args ...interface{}) {
fmt.Fprintf(os.Stderr, s+"\n", args...)
os.Exit(1)
}
func matchString(a, b string) (bool, error) {
return a == b, nil
}
func main() {
flag.Parse()
if *testName == "" {
fatalf("test-name flag must be provided")
}
log.SetLevel(log.Warning)
if *debug {
log.SetLevel(log.Debug)
}
if *platform != "native" && *runscPath == "" {
if err := testutil.ConfigureExePath(); err != nil {
panic(err.Error())
}
*runscPath = specutils.ExePath
}
// Make sure stdout and stderr are opened with O_APPEND, otherwise logs
// from outside the sandbox can (and will) stomp on logs from inside
// the sandbox.
for _, f := range []*os.File{os.Stdout, os.Stderr} {
flags, err := unix.FcntlInt(f.Fd(), unix.F_GETFL, 0)
if err != nil {
fatalf("error getting file flags for %v: %v", f, err)
}
if flags&unix.O_APPEND == 0 {
flags |= unix.O_APPEND
if _, err := unix.FcntlInt(f.Fd(), unix.F_SETFL, flags); err != nil {
fatalf("error setting file flags for %v: %v", f, err)
}
}
}
// Get path to test binary.
fullTestName := filepath.Join(testDir, *testName)
testBin, err := testutil.FindFile(fullTestName)
if err != nil {
fatalf("FindFile(%q) failed: %v", fullTestName, err)
}
// Get all test cases in each binary.
testCases, err := gtest.ParseTestCases(testBin)
if err != nil {
fatalf("ParseTestCases(%q) failed: %v", testBin, err)
}
// If sharding, then get the subset of tests to run based on the shard index.
if indexStr, totalStr := os.Getenv("TEST_SHARD_INDEX"), os.Getenv("TEST_TOTAL_SHARDS"); indexStr != "" && totalStr != "" {
// Parse index and total to ints.
index, err := strconv.Atoi(indexStr)
if err != nil {
fatalf("invalid TEST_SHARD_INDEX %q: %v", indexStr, err)
}
total, err := strconv.Atoi(totalStr)
if err != nil {
fatalf("invalid TEST_TOTAL_SHARDS %q: %v", totalStr, err)
}
// Calculate subslice of tests to run.
shardSize := int(math.Ceil(float64(len(testCases)) / float64(total)))
begin := index * shardSize
// Set end as begin of next subslice.
end := ((index + 1) * shardSize)
if begin > len(testCases) {
// Nothing to run.
return
}
if end > len(testCases) {
end = len(testCases)
}
testCases = testCases[begin:end]
}
var tests []testing.InternalTest
for _, tc := range testCases {
// Capture tc.
tc := tc
testName := fmt.Sprintf("%s_%s", tc.Suite, tc.Name)
tests = append(tests, testing.InternalTest{
Name: testName,
F: func(t *testing.T) {
if *parallel {
t.Parallel()
}
if *platform == "native" {
// Run the test case on host.
runTestCaseNative(testBin, tc, t)
} else {
// Run the test case in runsc.
runTestCaseRunsc(testBin, tc, t)
}
},
})
}
testing.Main(matchString, tests, nil, nil)
}