// 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] ", st.TestName, title) } else if err := logImageWithFrameAndPrefix(ctx, screenshot, title, fmt.Sprintf("[%s] ", st.TestName)); err != nil { log("[%s:%s] ", 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) }