Files
Etienne PerotandgVisor bot 5e6589e0b7 Update CUDA test compatibility to keep up with added gVisor support.
These CUDA tests were initially broken in gVisor but now appear to pass.

The test now also verifies that all capabilities are enabled when running.

PiperOrigin-RevId: 713094806
2025-01-07 17:28:00 -08:00

1066 lines
33 KiB
Go

// Copyright 2024 The gVisor Authors.
//
// 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.
// run_sample runs a CUDA sample test.
// These tests are complicated because some of them involve X windows,
// as opposed to traditional command-line-only tests.
// This binary handles all types of CUDA sample tests.
//
// To run: /run_sample [--timeout=15m] test1 test2 test3 ...
package main
import (
"bufio"
"bytes"
"context"
"errors"
"flag"
"fmt"
"image"
"image/draw"
"image/png"
"io"
"io/fs"
"os"
"os/exec"
"path"
"path/filepath"
"strconv"
"strings"
"sync"
"syscall"
"time"
)
// Flags.
var (
timeoutFlag = flag.Duration("timeout", 15*time.Minute, "Timeout for the program before it must clean up")
)
const (
// xDisplay is the X server address.
xDisplay = ":0"
)
// logMu protects log output.
var logMu sync.Mutex
// log logs a message to stderr. `format` should not have a newline.
// This does not use the standard logging library because this program needs
// to support logging multiple lines atomically.
func log(format string, values ...any) {
logDo(func() {
fmt.Fprintf(os.Stderr, "%s\n", fmt.Sprintf(format, values...))
})
}
// logDo runs a function while logging the log lock.
// This is useful to log multiple lines at a time.
func logDo(fn func()) {
logMu.Lock()
defer logMu.Unlock()
fn()
}
// logWriter implements io.Writer and logs to stderr.
type logWriter struct{}
func (w *logWriter) Write(p []byte) (n int, err error) {
logDo(func() {
n, err = os.Stderr.Write(p)
})
return n, err
}
// Command wraps a command with some niceties for stdout/stderr handling.
type Command struct {
// Cmd is the wrapped command.
Cmd *exec.Cmd
// Option fields.
// If non-nil, this data will be fed to the command's stdin.
Stdin []byte
// ForwardStdout and ForwardStderr control whether stdout/stderr are
// forwarded to the user's console.
ForwardStdout, ForwardStderr bool
// PrefixStdout and PrefixStderr are prefixes for forwarded logs.
PrefixStdout, PrefixStderr string
// streamWG waits for stdout/stderr capturing goroutines.
streamWG sync.WaitGroup
// mu protects the fields below.
mu sync.Mutex
// started is `true` if the command has started.
started bool
// Sets of stdout/stderr/combined output lines.
stdoutLines, stderrLines, combined []string
// waitErr is the error returned by `Cmd.Wait`.
waitErr error
// doneCh is closed when the command is done running.
doneCh chan struct{}
}
// Start starts a command in the background.
func (c *Command) Start(ctx context.Context) error {
c.mu.Lock()
defer c.mu.Unlock()
if c.started {
return errors.New("command already started")
}
for _, env := range os.Environ() {
c.Cmd.Env = append(c.Cmd.Env, env)
}
if len(c.Stdin) == 0 {
c.Cmd.Stdin = nil // Read from /dev/null
} else {
c.Cmd.Stdin = bytes.NewReader(c.Stdin)
}
stdout, err := c.Cmd.StdoutPipe()
if err != nil {
return fmt.Errorf("cannot open stdout pipe: %w", err)
}
stderr, err := c.Cmd.StderrPipe()
if err != nil {
return fmt.Errorf("cannot open stderr pipe: %w", err)
}
if err := c.Cmd.Start(); err != nil {
return fmt.Errorf("cannot start command: %w", err)
}
c.started = true
for _, stream := range []struct {
forward bool
prefix string
from io.ReadCloser
to io.Writer
lines *[]string
}{
{c.ForwardStdout, c.PrefixStdout, stdout, os.Stdout, &c.stdoutLines},
{c.ForwardStderr, c.PrefixStderr, stderr, &logWriter{}, &c.stderrLines},
} {
c.streamWG.Add(1)
go func(forward bool, prefix string, from io.ReadCloser, to io.Writer, lines *[]string) {
defer c.streamWG.Done()
for scanner := bufio.NewScanner(from); scanner.Scan(); {
text := scanner.Text()
c.mu.Lock()
*lines = append(*lines, text)
c.combined = append(c.combined, text)
if forward {
fmt.Fprintf(to, "%s%s\n", prefix, text)
}
c.mu.Unlock()
}
}(stream.forward, stream.prefix, stream.from, stream.to, stream.lines)
}
c.doneCh = make(chan struct{})
go func() {
c.streamWG.Wait()
c.mu.Lock()
defer c.mu.Unlock()
c.waitErr = c.Cmd.Wait()
close(c.doneCh)
}()
return nil
}
// Stdout returns the standard output lines of the command so far.
func (c *Command) Stdout() []string {
c.mu.Lock()
defer c.mu.Unlock()
return c.stdoutLines[:]
}
// Stderr returns the standard error lines of the command so far.
func (c *Command) Stderr() []string {
c.mu.Lock()
defer c.mu.Unlock()
return c.stderrLines[:]
}
// Combined returns the combined stodut/stderr lines of the command so far.
// This is not the same as stdout concatenated with stderr, as it preserves
// line ordering as they were emitted.
func (c *Command) Combined() []string {
c.mu.Lock()
defer c.mu.Unlock()
return c.combined[:]
}
// PID returns the PID of the running command.
func (c *Command) PID() int {
return c.Cmd.Process.Pid
}
// ExitCode returns the exit code of the command.
func (c *Command) ExitCode(ctx context.Context) (int, error) {
c.mu.Lock()
if !c.started {
c.mu.Unlock()
return 0, errors.New("command not started")
}
select {
case <-ctx.Done():
return 0, ctx.Err()
case <-c.Done():
}
c.mu.Lock()
defer c.mu.Unlock()
if c.waitErr == nil {
return 0, nil
}
if exitErr := (*exec.ExitError)(nil); errors.As(c.waitErr, &exitErr) {
return exitErr.ExitCode(), nil
}
return 0, fmt.Errorf("process exit did not carry exit code: %w", c.waitErr)
}
// Wait waits for a `Start`ed command to run to completion and returns
// stdout/stderr.
func (c *Command) Wait(ctx context.Context) ([]string, []string, error) {
c.mu.Lock()
if !c.started {
c.mu.Unlock()
return nil, nil, errors.New("command not started")
}
c.mu.Unlock()
select {
case <-ctx.Done():
case <-c.Done():
}
stdout := c.Stdout()
stderr := c.Stderr()
c.mu.Lock()
err := c.waitErr
c.mu.Unlock()
if err != nil {
return stdout, stderr, fmt.Errorf("command failed: %w", err)
}
return stdout, stderr, err
}
// Run `Start`s and `Wait`s for a command to run to completion.
func (c *Command) Run(ctx context.Context) ([]string, []string, error) {
if err := c.Start(ctx); err != nil {
return nil, nil, err
}
return c.Wait(ctx)
}
// CombinedOutput runs a command to completion and returns combined
// stdout/stderr output.
func (c *Command) CombinedOutput(ctx context.Context) (string, error) {
if err := c.Start(ctx); err != nil {
return "", err
}
_, _, err := c.Wait(ctx)
return strings.Join(c.Combined(), "\n"), err
}
// Done returns a channel that is closed when the command terminates.
// Must be called after `Start`.
func (c *Command) Done() <-chan struct{} {
c.mu.Lock()
defer c.mu.Unlock()
if c.doneCh == nil {
panic("Command.Done called before Command.Start")
}
return c.doneCh
}
// Terminate terminates a process.
// It does not reap the process; the caller should call wait if appropriate.
func Terminate(ctx context.Context, pid int, waitChans ...<-chan struct{}) error {
unifiedWaitChan := make(chan struct{})
waitShutdown := make(chan struct{})
defer close(waitShutdown)
for _, waitChan := range waitChans {
go func(waitChan <-chan struct{}) {
select {
case <-waitShutdown:
case <-waitChan:
unifiedWaitChan <- struct{}{}
}
}(waitChan)
}
// Ignore errors here because it doesn't matter; we will re-detect
// the post-signal process state later.
_ = syscall.Kill(pid, syscall.SIGTERM)
select {
case <-ctx.Done():
case <-time.After(5 * time.Second):
case <-unifiedWaitChan:
}
if _, err := os.Stat(fmt.Sprintf("/proc/%d", pid)); err != nil && os.IsNotExist(err) {
// The process is gone, so we are successful.
return nil
}
// Otherwise, send SIGKILL.
if err := syscall.Kill(pid, syscall.SIGKILL); err != nil {
return fmt.Errorf("cannot send SIGKILL: %w", err)
}
return nil
}
// XServer represents an X server.
type XServer struct {
xvfb *Command
}
// NewXServer creates a new X server.
func NewXServer(ctx context.Context) (*XServer, error) {
xvfb := &Command{
Cmd: exec.CommandContext(ctx, "Xvfb", xDisplay, "-screen", "0", "1920x1080x24"),
ForwardStdout: true,
PrefixStdout: "[Xvfb:stdout] ",
ForwardStderr: true,
PrefixStderr: "[Xvfb:stderr] ",
}
if err := xvfb.Start(ctx); err != nil {
return nil, fmt.Errorf("cannot start X server: %w", err)
}
x := &XServer{xvfb: xvfb}
if err := x.Probe(ctx); err != nil {
x.Shutdown(ctx)
return nil, fmt.Errorf("X server did not start in time: %w", err)
}
return x, nil
}
// Env returns the DISPLAY environment variable to use for this X server.
func (x *XServer) Env() string {
return fmt.Sprintf("DISPLAY=%s", xDisplay)
}
// Command returns a command that runs in the context of this X server.
func (x *XServer) Command(ctx context.Context, argv ...string) *Command {
cmd := &Command{Cmd: exec.CommandContext(ctx, argv[0], argv[1:]...)}
cmd.Cmd.Env = append(cmd.Cmd.Env, x.Env())
return cmd
}
// Probe probes the X server to see if it is alive.
func (x *XServer) Probe(ctx context.Context) error {
probeCtx, probeCancel := context.WithTimeout(ctx, 10*time.Second)
defer probeCancel()
lastErr := ctx.Err()
for probeCtx.Err() == nil {
output, err := x.Command(probeCtx, "xset", "q").CombinedOutput(ctx)
if err == nil {
return nil
}
lastErr = fmt.Errorf("cannot probe X server: %w: %s", err, output)
}
return lastErr
}
// Shutdown attempts to shut down the X server.
func (x *XServer) Shutdown(ctx context.Context) error {
if err := Terminate(ctx, x.xvfb.Cmd.Process.Pid, x.xvfb.Done()); err != nil {
return fmt.Errorf("cannot shut down Xvfb: %w", err)
}
_, _, _ = x.xvfb.Wait(ctx) // Reap, ignore errors.
return nil
}
// XWindow represents a window in the X server.
type XWindow struct {
x *XServer
id int64
}
// Windows returns a list of X windows.
func (x *XServer) Windows(ctx context.Context) ([]*XWindow, error) {
cmd := x.Command(ctx, "xdotool", "search", "--all", ".*")
stdout, _, err := cmd.Run(ctx)
if err != nil {
return nil, fmt.Errorf("xdotool search failed: %w (output: %v)", err, cmd.Combined())
}
windows := make([]*XWindow, 0, len(stdout))
for _, line := range stdout {
line = strings.TrimSpace(line)
if line == "" {
continue
}
windowID, err := strconv.Atoi(line)
if err != nil {
return nil, fmt.Errorf("unexpected xdotool output: %q (whole output: %v)", line, cmd.Combined())
}
windows = append(windows, &XWindow{x: x, id: int64(windowID)})
}
return windows, nil
}
// ID returns a the window ID as a string.
func (w *XWindow) ID() string {
return fmt.Sprintf("%d", w.id)
}
// String returns a string containing the window ID.
func (w *XWindow) String() string {
return fmt.Sprintf("window:%d", w.id)
}
// Title returns the window title.
func (w *XWindow) Title(ctx context.Context) (string, error) {
cmd := w.x.Command(ctx, "xdotool", "getwindowname", w.ID())
stdout, stderr, err := cmd.Wait(ctx)
if err != nil {
return "", w.diagnoseErr(ctx, fmt.Errorf("cannot get window %s title: %w (%q)", w, err, strings.Join(stderr, "\n")))
}
if len(stdout) != 1 || stdout[0] == "" {
return "", w.diagnoseErr(ctx, fmt.Errorf("cannot get window %s title: unexpected output %q", w, strings.Join(stdout, "\n")))
}
return stdout[0], nil
}
// PID returns the PID controlling the window.
// Note that this information is only optionally specified by a process
// creating a window, and is never guaranteed to be there.
func (w *XWindow) PID(ctx context.Context) (int, error) {
cmd := w.x.Command(ctx, "xdotool", "getwindowpid", w.ID())
stdout, stderr, err := cmd.Wait(ctx)
if err != nil {
return -1, w.diagnoseErr(ctx, fmt.Errorf("cannot get window %s PID: %w (%q)", w, err, strings.Join(stderr, "\n")))
}
if len(stdout) != 1 || stdout[0] == "" {
return -1, w.diagnoseErr(ctx, fmt.Errorf("cannot get window %s PID: unexpected output %q", w, strings.Join(stdout, "\n")))
}
pid, err := strconv.Atoi(stdout[0])
if err != nil {
return -1, w.diagnoseErr(ctx, fmt.Errorf("cannot get window %s PID: invalid PID %q: %w", w, stdout[0], err))
}
return pid, nil
}
// Activate activates or focuses the X window.
func (w *XWindow) Activate(ctx context.Context) error {
cmd := w.x.Command(ctx, "xdotool", "windowactivate", "--sync", w.ID())
if output, err := cmd.CombinedOutput(ctx); err != nil {
return w.diagnoseErr(ctx, fmt.Errorf("xdotool windowactivate: %w (output: %q)", err, output))
}
return nil
}
// Keystroke sends a keystroke to the X window.
func (w *XWindow) Keystroke(ctx context.Context, keystrokes ...string) error {
cmd := w.x.Command(
ctx,
append(
[]string{
"xdotool",
"key",
"--clearmodifiers",
"--window",
w.ID(),
},
keystrokes...)...)
if output, err := cmd.CombinedOutput(ctx); err != nil {
return w.diagnoseErr(ctx, fmt.Errorf("xdotool key: %w (output: %q)", err, output))
}
return nil
}
// Screenshot takes a screenshot image of the X window.
func (w *XWindow) Screenshot(ctx context.Context) (image.Image, error) {
screenshotCtx, screenshotCancel := context.WithTimeout(ctx, 10*time.Second)
// Need to use a raw `exec.Command` here because stdout is a byte stream
// as opposed to a text stream.
cmd := exec.CommandContext(screenshotCtx, "import", "-window", w.ID(), "png:-" /* Save to stdout as PNG */)
cmd.Env = append(cmd.Env, w.x.Env())
var stdoutBuf, stderrBuf bytes.Buffer
cmd.Stdout = &stdoutBuf
cmd.Stderr = &stderrBuf
err := cmd.Run()
screenshotCancel()
stderr := string(stderrBuf.Bytes())
if err != nil {
// Best-effort attempt to kill the process.
_ = Terminate(ctx, cmd.Process.Pid)
return nil, w.diagnoseErr(ctx, fmt.Errorf("imagemagick failed: %w (output: %q)", err, stderr))
}
img, err := png.Decode(&stdoutBuf)
if err != nil {
return nil, w.diagnoseErr(ctx, fmt.Errorf("cannot decode screenshot image: %w (output: %q)", err, stderr))
}
if size := img.Bounds().Size(); size.X == 0 || size.Y == 0 {
return nil, w.diagnoseErr(ctx, fmt.Errorf("screenshot image has zero dimension (output: %q)", stderr))
}
return img, nil
}
// diagnoseErr annotates an error with additional window information.
func (w *XWindow) diagnoseErr(ctx context.Context, err error) error {
if err == nil {
return nil
}
probeCtx, probeCancel := context.WithTimeout(ctx, 1*time.Second)
defer probeCancel()
if xErr := w.x.Probe(probeCtx); xErr != nil {
return fmt.Errorf("%w (X server is down: %v)", err, xErr)
}
winInfo, infoErr := w.x.Command(ctx, "xwininfo", "-id", w.ID()).CombinedOutput(ctx)
if infoErr != nil {
return fmt.Errorf("%w (cannot get window info: %v - %q)", err, infoErr, winInfo)
}
return fmt.Errorf("%w (window info: %q)", err, winInfo)
}
// SampleTest represents a single sample test to execute.
type SampleTest struct {
TestName string
XServer *XServer
}
// NewSampleTest creates a new SampleTest.
func NewSampleTest(testName string, x *XServer) (*SampleTest, error) {
st := &SampleTest{TestName: testName, XServer: x}
if _, err := os.Stat(st.dir()); err != nil {
return nil, fmt.Errorf("invalid test %q: directory %q: %w", st.TestName, st.dir(), err)
}
return st, nil
}
// dir returns the test directory.
func (st *SampleTest) dir() string {
const samplesRoot = "/cuda-samples/Samples"
return path.Join(samplesRoot, st.TestName)
}
// cmd returns a `*Command` with proper environment variables and
// working directory for the test. Its output is forwarded to the console.
func (st *SampleTest) cmd(ctx context.Context, argv ...string) *Command {
argv0Base := path.Base(argv[0])
cmd := st.XServer.Command(ctx, argv...)
cmd.Cmd.Dir = st.dir()
cmd.ForwardStdout = true
cmd.PrefixStdout = fmt.Sprintf("[%s:%s:stdout] ", st.TestName, argv0Base)
cmd.ForwardStderr = true
cmd.PrefixStderr = fmt.Sprintf("[%s:%s:stderr] ", st.TestName, argv0Base)
return cmd
}
// quietCmd returns a `*Command` with proper environment variables and
// working directory for the test. Its output is not forwarded to the console.
func (st *SampleTest) quietCmd(ctx context.Context, argv ...string) *Command {
cmd := st.cmd(ctx, argv...)
cmd.ForwardStdout = false
cmd.ForwardStderr = false
return cmd
}
// SampleState captures states that is captured before a test runs, and that
// is useful to refer to while (or after) the test is running.
type SampleState struct {
// When is the timestamp at which this SampleState was taken.
When time.Time
// Executables holds clean paths of all executable files in the test dir.
Executables map[string]struct{}
// Windows is a list of window screenshots in the X server, mapped by ID.
Windows map[string]*XWindow
// Screenshots is a list of screenshots mapped by window ID.
// If a screenshot fails, the window is mapped to `nil`.
Screenshots map[string]image.Image
}
// NewExecutables returns the executables in `after` that are not in `ss`.
func (ss *SampleState) NewExecutables(after *SampleState) []string {
newExecutables := make([]string, 0, len(after.Executables))
for e := range after.Executables {
if _, found := ss.Executables[e]; !found {
newExecutables = append(newExecutables, e)
}
}
return newExecutables
}
// DifferentWindows returns the windows in `after` that are new or for which
// the screenshot has changed.
func (ss *SampleState) DifferentWindows(after *SampleState) []*XWindow {
diffWindows := make([]*XWindow, 0, len(after.Windows))
for id, window := range after.Windows {
if _, found := ss.Windows[id]; !found {
diffWindows = append(diffWindows, window)
continue
}
if !imgEq(ss.Screenshots[id], after.Screenshots[id]) {
diffWindows = append(diffWindows, window)
}
}
return diffWindows
}
// imgEq returns true if the two given images are identical in size and pixel
// values.
func imgEq(a, b image.Image) bool {
if a == nil && b == nil {
return true
}
if a == nil || b == nil {
return false
}
bounds := a.Bounds()
if bounds != b.Bounds() {
return false
}
// Convert images to RGBA so that we can compare raw pixel data directly.
imgA := image.NewRGBA(bounds)
draw.Draw(imgA, bounds, a, image.Point{0, 0}, draw.Src)
imgB := image.NewRGBA(bounds)
draw.Draw(imgB, bounds, b, image.Point{0, 0}, draw.Src)
if imgA.Stride != imgB.Stride || imgA.Rect != imgB.Rect || len(imgA.Pix) != len(imgB.Pix) {
return false
}
for i := 0; i < len(imgA.Pix); i++ {
if imgA.Pix[i] != imgB.Pix[i] {
return false
}
}
return true
}
// logImageWithPrefix renders an image to text, frames it with the given
// title, and logs that with a given prefix.
func logImageWithFrameAndPrefix(ctx context.Context, img image.Image, title, prefix string) error {
const imageWidth = 72
var pngBytes bytes.Buffer
if err := png.Encode(&pngBytes, img); err != nil {
return fmt.Errorf("png encoding failed: %v", err)
}
stdout, stderr, err := (&Command{
Cmd: exec.CommandContext(ctx, "ascii-image-converter", "/dev/stdin", fmt.Sprintf("--width=%d", imageWidth), "--braille", "--dither"),
Stdin: pngBytes.Bytes(),
}).Run(ctx)
if err != nil {
return fmt.Errorf("ascii-image-converter failed: %v (output: %q)", err, strings.Join(stderr, "\n"))
}
header := "┍"
footer := "╰"
numHeaderHorizontalLines := imageWidth - len(title) - 2
leftHeaderHorizontalLines := numHeaderHorizontalLines / 2
rightHeaderHorizontalLines := numHeaderHorizontalLines - leftHeaderHorizontalLines
for i := 0; i < leftHeaderHorizontalLines; i++ {
header += "━"
}
header += fmt.Sprintf(" %s ", title)
for i := 0; i < rightHeaderHorizontalLines; i++ {
header += "━"
}
for i := 0; i < imageWidth; i++ {
footer += "─"
}
header += "┑"
footer += "╯"
logDo(func() {
fmt.Fprintf(os.Stderr, "%s%s\n", prefix, header)
for _, line := range stdout {
fmt.Fprintf(os.Stderr, "%s|%s|\n", prefix, line)
}
fmt.Fprintf(os.Stderr, "%s%s\n", prefix, footer)
})
return nil
}
// State returns the current state of the test.
func (st *SampleTest) State(ctx context.Context) (*SampleState, error) {
when := time.Now()
executables := make(map[string]struct{})
err := filepath.Walk(st.dir(), func(path string, info fs.FileInfo, err error) error {
if err != nil {
return fmt.Errorf("cannot walk %q (%q): %w", st.dir(), path, err)
}
if !info.IsDir() && info.Mode()&0111 != 0 {
executables[path] = struct{}{}
}
return nil
})
if err != nil {
return nil, fmt.Errorf("cannot list executables: %w", err)
}
windows, err := st.XServer.Windows(ctx)
if err != nil {
return nil, fmt.Errorf("cannot list windows: %w", err)
}
windowMap := make(map[string]*XWindow, len(windows))
screenshots := make(map[string]image.Image, len(windows))
for _, w := range windows {
windowMap[w.ID()] = w
if screenshot, err := w.Screenshot(ctx); err == nil {
screenshots[w.ID()] = screenshot
}
}
return &SampleState{
When: when,
Executables: executables,
Windows: windowMap,
Screenshots: screenshots,
}, nil
}
// makeRun runs `make run` or `make testrun` in the test directory.
func (st *SampleTest) makeRun(ctx context.Context) (*Command, error) {
arch, err := st.quietCmd(ctx, "uname", "-m").CombinedOutput(ctx)
if err != nil || arch == "" {
return nil, fmt.Errorf("cannot get architecture (%q): %w", arch, err)
}
// All samples have a "testrun" make target. However, most of them have it
// set to do literally nothing.
// All samples also have a "run" make target. Unlike the "testrun" target,
// "run" always does something.
// However, when "testrun" actually does something, it is usually for the
// explicit purpose of running a test.
// For example, `0_Introduction/simpleTexture3D` has a `testrun` target that
// runs the file with an example texture file, whereas the `run` target
// opens a file passed as argument, which does not exist here.
// So we must detect the case where "testrun" does something useful vs the
// case where it does not.
// To do this, we parse the Makefile a bit to see if the `testrun` target
// contains any actual commands, as opposed to only containing build
// dependencies.
makefilePath := path.Join(st.dir(), "Makefile")
makefile, err := os.Open(makefilePath)
if err != nil {
return nil, fmt.Errorf("cannot open %q: %w", makefilePath, err)
}
defer makefile.Close()
testRunTargetHasCommands := false
for scanner := bufio.NewScanner(makefile); scanner.Scan(); {
line := scanner.Text()
if !strings.HasPrefix(line, "testrun:") {
continue
}
if !scanner.Scan() {
break
}
nextLine := scanner.Text()
if strings.HasPrefix(nextLine, "\t") && strings.TrimSpace(nextLine) != "" {
testRunTargetHasCommands = true
}
break
}
argv := []string{"make", "-C", st.dir(), fmt.Sprintf("TARGET_ARCH=%s", arch)}
if testRunTargetHasCommands {
argv = append(argv, "testrun")
} else {
argv = append(argv, "run")
}
log("[%s] Executing: %v", st.TestName, strings.Join(argv, " "))
cmd := st.cmd(ctx, argv...)
if err := cmd.Start(ctx); err != nil {
return nil, fmt.Errorf("cannot start `make`: %w", err)
}
return cmd, nil
}
// Run runs a single sample test.
func (st *SampleTest) Run(ctx context.Context) error {
const libNVVMTestDir = "7_libNVVM/"
if strings.HasPrefix(st.TestName, libNVVMTestDir) {
return st.RunLibNVVMTest(ctx)
}
if _, _, err := st.cmd(ctx, "make", "-C", st.dir(), "clean").Run(ctx); err != nil {
return fmt.Errorf("cannot run `make clean`: %w", err)
}
stateBefore, err := st.State(ctx)
if err != nil {
return fmt.Errorf("cannot get state before test: %w", err)
}
makeRun, err := st.makeRun(ctx)
if err != nil {
return fmt.Errorf("cannot run `make run`: %w", err)
}
defer Terminate(ctx, makeRun.PID())
// There are multiple possibilities here.
// Some CUDA programs will run an X application that runs forever.
// In this case, we need to detect this and to make sure it runs,
// then kill it.
// Other programs are just command-line based and run to completion,
// and we rely on their exit code.
// To determine this, we first just wait for a few seconds and see what
// the command does.
if err := st.Monitor(ctx, makeRun, stateBefore); err != nil {
return fmt.Errorf("test failed in `make run`: %w", err)
}
// Some `make` targets will silently exist with code 0 even if the test
// was actually unsuccessful because it cannot be built.
// To detect this case, we look for the absence of any executable file in
// the sample directory. All `make` targets should create an executable, and
// this won't happen if `make` bails out.
stateAfter, err := st.State(ctx)
if err != nil {
return fmt.Errorf("cannot get state after test: %w", err)
}
if len(stateBefore.NewExecutables(stateAfter)) == 0 {
return fmt.Errorf("did not find any new executable file created by `make run` in the test directory %q (existing executables: %v)", st.dir(), stateBefore.Executables)
}
return nil
}
// Monitor monitors whether a `make run` command terminates quickly or
// produces an X window.
func (st *SampleTest) Monitor(ctx context.Context, makeRun *Command, stateBefore *SampleState) error {
fastTicker := time.NewTicker(200 * time.Millisecond)
defer fastTicker.Stop()
var currentState *SampleState
for windowsChanged := false; !windowsChanged; {
select {
case <-ctx.Done(): // Context expired.
return ctx.Err()
case <-makeRun.Done(): // `make run` finished on its own.
_, _, err := makeRun.Wait(ctx)
return err
case <-fastTicker.C:
// Check for new windows.
var err error
currentState, err = st.State(ctx)
if err != nil {
return fmt.Errorf("cannot get test state: %w", err)
}
windowsChanged = len(stateBefore.DifferentWindows(currentState)) > 0
}
}
// If we get here, the test produces X windows. So we need to monitor them.
// We will consider the test a success in any of the following cases:
// - The `make run` process exits at any time with a 0 exit code.
// - The set of windows stops changing for 3 consecutive seconds, i.e.
// the test has reached a stable steady state without crashing.
// - The set of windows continuously changes for 10 consecutive seconds,
// i.e. the test is likely a visually-changing demo over time and has
// reached a steady state without crashing.
log("[%s] This appears to be a test that uses graphics and X windows.", st.TestName)
lastState := stateBefore
slowTicker := time.NewTicker(1 * time.Second)
defer slowTicker.Stop()
lastWindowChange := currentState.When
successDeadline := time.After(10 * time.Second)
for {
select {
case <-ctx.Done(): // Context expired.
return ctx.Err()
case <-makeRun.Done(): // `make run` finished on its own.
_, _, err := makeRun.Wait(ctx)
return err
case <-successDeadline: // Still no crashes after long enough.
return st.TerminateWindowTest(ctx, makeRun, stateBefore)
case <-slowTicker.C:
stateNow, err := st.State(ctx)
if err != nil {
return fmt.Errorf("cannot get test state: %w", err)
}
if differentWindows := lastState.DifferentWindows(stateNow); len(differentWindows) > 0 {
lastWindowChange = stateNow.When
log("[%s] [%s] Windows changed:", st.TestName, stateNow.When.Format("15:04:05"))
for _, window := range differentWindows {
title, err := window.Title(ctx)
if err != nil {
title = window.String()
}
if screenshot := stateNow.Screenshots[window.ID()]; screenshot == nil {
log("[%s:%s] <screenshot failed>", st.TestName, title)
} else if err := logImageWithFrameAndPrefix(ctx, screenshot, title, fmt.Sprintf("[%s] ", st.TestName)); err != nil {
log("[%s:%s] <rendering screenshot failed: %v>", st.TestName, title, err)
}
}
}
if currentState.When.Sub(lastWindowChange) >= 3*time.Second {
return st.TerminateWindowTest(ctx, makeRun, stateBefore)
}
lastState = stateNow
}
}
}
// TerminateWindowTest terminates a sample test that produces X windows.
func (st *SampleTest) TerminateWindowTest(ctx context.Context, makeRun *Command, stateBefore *SampleState) error {
stateNow, err := st.State(ctx)
if err != nil {
return fmt.Errorf("cannot get test state: %w", err)
}
testWindows := stateBefore.DifferentWindows(stateNow)
// Most windows-based tests accept typing the letter "Q" to quit them.
// Try it first.
for _, window := range testWindows {
// Ignore error for both activation and keystrokes; this is just a
// best-effort attempt to press "Q".
_ = window.Activate(ctx)
_ = window.Keystroke(ctx, "q")
}
// Now wait a little bit to see if the program ends on its own from that.
select {
case <-ctx.Done():
return ctx.Err()
case <-makeRun.Done():
_, _, err = makeRun.Wait(ctx)
return err
case <-time.After(3 * time.Second):
// Didn't work, keep going.
}
// Gather a list of test PIDs.
windowPIDs := make(map[int]struct{})
for _, window := range testWindows {
pid, err := window.PID(ctx)
if err != nil {
// X window PID information is optional; erroring out here is not
// appropriate.
continue
}
if pid == makeRun.PID() {
continue
}
windowPIDs[pid] = struct{}{}
}
if len(windowPIDs) > 0 {
// Kill all the PIDs we gathered.
for pid := range windowPIDs {
_ = Terminate(ctx, pid, makeRun.Done())
}
// Now check if `make run` terminates on its own.
select {
case <-ctx.Done():
return ctx.Err()
case <-makeRun.Done():
_, _, err = makeRun.Wait(ctx)
return err
case <-time.After(3 * time.Second):
// Didn't work, keep going.
}
}
return errors.New("test did not terminate")
}
// RunLibNVVMTest runs a `libnvvm`-based test.
// These tests are located in the `7_libNVVM/` directory.
func (st *SampleTest) RunLibNVVMTest(ctx context.Context) error {
const ptxgenTestName = "ptxgen"
// Need to run `cmake` in the 7_libNVVM/ directory to build the test.
libNVVMTestsDir := path.Dir(st.dir())
libNVVMTestName := path.Base(st.dir())
cmake := st.cmd(ctx, "cmake", ".")
cmake.Cmd.Dir = libNVVMTestsDir
if _, _, err := cmake.Run(ctx); err != nil {
return fmt.Errorf("cannot run `cmake`: %w", err)
}
// Then run `make` in the test directory.
// CMake generates a make file in the parent directory.
// We `make all` rather than just the test target, because
// `cuda-c-linking` depends on the `mathfuncs` target despite not being
// declared as such in the Makefile.
arch, err := st.quietCmd(ctx, "uname", "-m").CombinedOutput(ctx)
if err != nil || arch == "" {
return fmt.Errorf("cannot get architecture (%q): %w", arch, err)
}
makeCmd := st.cmd(ctx, "make", "-C", libNVVMTestsDir, fmt.Sprintf("TARGET_ARCH=%s", arch), "all")
if _, _, err := makeCmd.Run(ctx); err != nil {
return fmt.Errorf("cannot run `make`: %w", err)
}
// `make` will create an executable in the test directory that has the same
// name as the directory does.
exePath := path.Join(st.dir(), libNVVMTestName)
if _, err := os.Stat(exePath); err != nil {
return fmt.Errorf("cannot stat executable at expected location %q: %w", exePath, err)
}
argv := []string{exePath}
if libNVVMTestName == ptxgenTestName {
// The ptxgen test binary needs a .ll file as input.
// Conveniently, there is one called "test.ll" in the test directory.
argv = append(argv, path.Join(st.dir(), "test.ll"))
}
if _, _, err := st.cmd(ctx, argv...).Run(ctx); err != nil {
return fmt.Errorf("test binary failed: %w", err)
}
return nil
}
// Main is the main method of this program.
func Main(ctx context.Context) (int, error) {
flag.Parse()
if nvCaps := os.Getenv("NVIDIA_DRIVER_CAPABILITIES"); nvCaps != "all" {
return 1, fmt.Errorf("NVIDIA_DRIVER_CAPABILITIES is not set to 'all' (got %q); please set it to 'all' and try again", nvCaps)
}
cleanupCtx, cleanupCancel := context.WithTimeout(ctx, *timeoutFlag)
defer cleanupCancel()
deadline, _ := cleanupCtx.Deadline()
x, err := NewXServer(cleanupCtx)
if err != nil {
return 1, fmt.Errorf("failed to start X server: %s", err)
}
defer x.Shutdown(cleanupCtx)
testsCtx, testsCancel := context.WithDeadline(cleanupCtx, deadline.Add(-10*time.Second))
defer testsCancel()
numTests := 0
exitCode := 1
var lastErr error
for _, testName := range flag.Args() {
numTests++
st, err := NewSampleTest(testName, x)
if err != nil {
log("> Invalid test %q: %s", testName, err)
lastErr = fmt.Errorf("invalid test %q: %w", testName, err)
continue
}
log("> Running test: %s", testName)
testCtx, testCancel := context.WithCancel(testsCtx)
err = st.Run(testCtx)
testCancel()
if err != nil {
log("> Test failed: %s (%s)", testName, err)
lastErr = fmt.Errorf("test %q failed: %w", testName, err)
if exitErr := (*exec.ExitError)(nil); errors.As(err, &exitErr) && exitErr.ExitCode() > 0 {
exitCode = exitErr.ExitCode()
}
continue
}
log("> Test passed: %s", testName)
}
if numTests == 0 {
return 1, fmt.Errorf("no tests to run, failing vacuously; specify test names as positional arguments")
}
if lastErr == nil {
return 0, nil
}
if numTests != 1 {
return 1, fmt.Errorf("one or more tests failed (last error: %w)", lastErr)
}
// If there was a single test to run, pass along its error code if it
// had one. (It may not have had one in case the test failed for another
// reason, e.g. error setting up the test prior to running it.)
if exitCode == 0 {
exitCode = 1
}
return exitCode, fmt.Errorf("test failed: %w", lastErr)
}
func main() {
exitCode, err := Main(context.Background())
if err != nil {
log("%s", err)
log("FAIL")
} else {
log("PASS")
}
os.Exit(exitCode)
}