Files

1066 lines
33 KiB
Go
Raw Permalink Normal View History

2024-04-19 19:10:11 -07:00
// 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)
}
2024-04-19 19:10:11 -07:00
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
2024-04-19 19:10:11 -07:00
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)
2024-04-19 19:10:11 -07:00
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)
2024-04-19 19:10:11 -07:00
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
2024-04-19 19:10:11 -07:00
}
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)
2024-04-19 19:10:11 -07:00
}
func main() {
exitCode, err := Main(context.Background())
if err != nil {
log("%s", err)
log("FAIL")
} else {
log("PASS")
}
os.Exit(exitCode)
}