blob: efb457dd0c6db1387bbaab8fc2f85bb669b25b67 [file] [log] [blame]
// Copyright 2023 The Dawn & Tint Authors
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are met:
//
// 1. Redistributions of source code must retain the above copyright notice, this
// list of conditions and the following disclaimer.
//
// 2. Redistributions in binary form must reproduce the above copyright notice,
// this list of conditions and the following disclaimer in the documentation
// and/or other materials provided with the distribution.
//
// 3. Neither the name of the copyright holder nor the names of its
// contributors may be used to endorse or promote products derived from
// this software without specific prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
package node
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"os"
"os/exec"
"path/filepath"
"strconv"
"strings"
"sync"
"time"
"dawn.googlesource.com/dawn/tools/src/cmd/run-cts/common"
"dawn.googlesource.com/dawn/tools/src/utils"
)
// runTestCasesWithServers spawns c.flags.NumRunners server instances to run all
// the test cases in testCases. The results of the tests are streamed to results.
// Blocks until all the tests have been run.
func (c *cmd) runTestCasesWithServers(ctx context.Context, testCases []common.TestCase, results chan<- common.Result) {
// Create a chan of test indices.
// This will be read by the test runner goroutines.
testCaseIndices := make(chan int, 256)
go func() {
for i := range testCases {
testCaseIndices <- i
}
close(testCaseIndices)
}()
// Spin up the test runner goroutines
wg := &sync.WaitGroup{}
for i := 0; i < c.flags.NumRunners; i++ {
id := i
wg.Add(1)
go func() {
defer wg.Done()
if err := c.runServer(ctx, id, testCases, testCaseIndices, results); err != nil {
results <- common.Result{
Status: common.Fail,
Error: fmt.Errorf("Test server error: %w", err),
}
}
}()
}
wg.Wait()
}
// runServer starts a test runner server instance, takes case indices from
// testCaseIndices, and requests the server run the test with the given index.
// The result of the test run is written to the results chan.
// Once the testCaseIndices chan has been closed and all taken tests have been
// completed, the server process is shutdown and runServer returns.
func (c *cmd) runServer(
ctx context.Context,
id int,
testCases []common.TestCase,
testCaseIndices <-chan int,
results chan<- common.Result) error {
var port int
testCaseLog := &bytes.Buffer{}
stopServer := func() {}
startServer := func() error {
args := []string{
"-e", "require('./out-node/common/runtime/server.js');",
"--",
// src/common/runtime/helper/sys.ts expects 'node file.js <args>'
// and slices away the first two arguments. When running with '-e', args
// start at 1, so just inject a placeholder argument.
"placeholder-arg",
// Actual arguments begin here
"--gpu-provider", filepath.Join(c.flags.bin, "cts.js"),
}
if c.flags.Colors {
args = append(args, "--colors")
}
if c.coverage != nil {
args = append(args, "--coverage")
}
if c.flags.Verbose {
args = append(args,
"--verbose",
"--gpu-provider-flag", "verbose=1")
}
if c.flags.unrollConstEvalLoops {
args = append(args, "--unroll-const-eval-loops")
}
if c.flags.compatibilityMode {
args = append(args, "--compat")
}
for _, f := range c.flags.dawn {
args = append(args, "--gpu-provider-flag", f)
}
cmd := exec.CommandContext(ctx, c.flags.Node, args...)
writer := io.Writer(testCaseLog)
if c.flags.Verbose {
pw := &utils.PrefixWriter{
Prefix: fmt.Sprintf("[%d] ", id),
Writer: os.Stdout,
}
writer = io.MultiWriter(pw, writer)
}
pl := newPortListener(writer)
cmd.Dir = c.flags.CTS
cmd.Stdout = &pl
cmd.Stderr = &pl
if c.flags.Verbose {
PrintCommand(cmd, c.flags.skipVSCodeInfo)
}
err := cmd.Start()
if err != nil {
return fmt.Errorf("failed to start test runner server: %v", err)
}
select {
case port = <-pl.port:
break // success
case <-time.After(time.Second * 10):
return fmt.Errorf("timeout waiting for server port:\n%v", pl.buffer.String())
case <-ctx.Done(): // cancelled
return ctx.Err()
}
// Load the cases
postResp, postErr := http.Post(fmt.Sprintf("http://localhost:%v/load?%v", port, c.query), "", &bytes.Buffer{})
if postErr != nil || postResp.StatusCode != http.StatusOK {
msg := &strings.Builder{}
fmt.Println(msg, "failed to load test cases: ", postErr)
if body, err := io.ReadAll(postResp.Body); err == nil {
fmt.Println(msg, string(body))
} else {
fmt.Println(msg, err)
}
return fmt.Errorf("%v", msg.String())
}
return nil
}
stopServer = func() {
if port > 0 {
go http.Post(fmt.Sprintf("http://localhost:%v/terminate", port), "", &bytes.Buffer{})
time.Sleep(time.Millisecond * 100)
port = 0
}
}
for idx := range testCaseIndices {
testCaseLog.Reset() // Clear the log for this test case
if port == 0 {
if err := startServer(); err != nil {
return err
}
}
res := common.Result{Index: idx, TestCase: testCases[idx]}
type Response struct {
Status string
Message string
CoverageData string
DurationMS float32
}
postResp, err := http.Post(fmt.Sprintf("http://localhost:%v/run?%v", port, testCases[idx]), "", &bytes.Buffer{})
if err != nil {
res.Error = fmt.Errorf("server POST failure. Restarting server... This can happen when there is a crash. Try running with --isolate.")
res.Status = common.Fail
results <- res
stopServer()
continue
}
if postResp.StatusCode == http.StatusOK {
var resp Response
if err := json.NewDecoder(postResp.Body).Decode(&resp); err != nil {
res.Error = fmt.Errorf("server response decode failure")
res.Status = common.Fail
results <- res
continue
}
res.Duration = time.Duration(resp.DurationMS*1000) * time.Microsecond
switch resp.Status {
case "pass":
res.Status = common.Pass
res.Message = resp.Message + testCaseLog.String()
case "warn":
res.Status = common.Warn
res.Message = resp.Message + testCaseLog.String()
case "fail":
res.Status = common.Fail
res.Message = resp.Message + testCaseLog.String()
case "skip":
res.Status = common.Skip
res.Message = resp.Message + testCaseLog.String()
default:
res.Status = common.Fail
res.Error = fmt.Errorf("unknown status: '%v'", resp.Status)
}
if resp.CoverageData != "" {
coverage, covErr := c.coverage.Env.Import(resp.CoverageData)
os.Remove(resp.CoverageData)
if covErr != nil {
if res.Message != "" {
res.Message += "\n"
}
res.Message += fmt.Sprintf("could not import coverage data from '%v': %v", resp.CoverageData, covErr)
}
res.Coverage = coverage
}
} else {
msg, err := io.ReadAll(postResp.Body)
if err != nil {
msg = []byte(err.Error())
}
res.Status = common.Fail
res.Error = fmt.Errorf("server error: %v", string(msg))
}
results <- res
}
stopServer()
return nil
}
// portListener implements io.Writer, monitoring written messages until a port
// is printed between '[[' ']]'. Once the port has been found, the parsed
// port number is written to the 'port' chan, and all subsequent writes are
// forwarded to writer.
type portListener struct {
writer io.Writer
buffer strings.Builder
port chan int
}
func newPortListener(w io.Writer) portListener {
return portListener{w, strings.Builder{}, make(chan int)}
}
func (p *portListener) Write(data []byte) (n int, err error) {
if p.port != nil {
p.buffer.Write(data)
str := p.buffer.String()
idx := strings.Index(str, "[[")
if idx < 0 {
// Still waiting for the opening '[['
return len(data), nil
}
str = str[idx+2:] // skip past '[['
idx = strings.Index(str, "]]")
if idx < 0 {
// Still waiting for the closing ']]'
return len(data), nil
}
port, err := strconv.Atoi(str[:idx])
if err != nil {
return 0, err
}
// Port found. Write it to the chan, and close the chan.
p.port <- port
close(p.port)
p.port = nil
str = strings.TrimRight(str[idx+2:], " \n")
if len(str) == 0 {
return len(data), nil
}
// Write out trailing text after the ']]'
return p.writer.Write([]byte(str))
}
// Port has been found. Just forward the rest of the data to p.writer
return p.writer.Write(data)
}