| // 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 chrome | |
| import ( | |
| "context" | |
| "encoding/json" | |
| "flag" | |
| "fmt" | |
| "log" | |
| "net" | |
| "net/http" | |
| "path/filepath" | |
| "strings" | |
| "sync" | |
| "time" | |
| "dawn.googlesource.com/dawn/tools/src/cmd/run-cts/common" | |
| "dawn.googlesource.com/dawn/tools/src/fileutils" | |
| "dawn.googlesource.com/dawn/tools/src/oswrapper" | |
| "github.com/chromedp/cdproto/runtime" | |
| "github.com/chromedp/chromedp" | |
| "golang.org/x/net/websocket" | |
| ) | |
| type flags struct { | |
| common.Flags | |
| chrome string | |
| } | |
| func init() { | |
| common.Register(&cmd{}) | |
| } | |
| type cmd struct { | |
| flags flags | |
| state common.State | |
| query string | |
| } | |
| func (c *cmd) Name() string { | |
| return "chrome" | |
| } | |
| func (c *cmd) Desc() string { | |
| return "runs the CTS with chrome" | |
| } | |
| func (c *cmd) RegisterFlags(ctx context.Context, cfg common.Config) ([]string, error) { | |
| c.flags.Register(cfg.OsWrapper) | |
| flag.StringVar(&c.flags.chrome, "chrome", "", "path to the chrome executable") | |
| return []string{"[query]"}, nil | |
| } | |
| // TODO(crbug.com/416755658): Add unittest coverage once there is a way to fake | |
| // the Chrome instance. | |
| func (c *cmd) Run(ctx context.Context, cfg common.Config) error { | |
| state, err := c.flags.Process(cfg.OsWrapper) | |
| if err != nil { | |
| return err | |
| } | |
| c.state = *state | |
| // The test query is the optional unnamed argument | |
| c.query = "webgpu:*" | |
| switch len(flag.Args()) { | |
| case 0: | |
| case 1: | |
| c.query = flag.Args()[0] | |
| default: | |
| return fmt.Errorf("only a single query can be provided") | |
| } | |
| if err := c.state.CTS.Node.BuildIfRequired(c.flags.Verbose, cfg.OsWrapper); err != nil { | |
| return err | |
| } | |
| if err := c.state.CTS.Standalone.BuildIfRequired(c.flags.Verbose, cfg.OsWrapper); err != nil { | |
| return err | |
| } | |
| // Find all the test cases that match r.query. | |
| testCases, err := c.state.CTS.QueryTestCases(c.flags.Verbose, c.query) | |
| if err != nil { | |
| return fmt.Errorf("failed to gather tests: %w", err) | |
| } | |
| fmt.Printf("Testing %d test cases...\n", len(testCases)) | |
| resultStream := make(chan common.Result, 256) | |
| go func() { | |
| c.runTestCases(ctx, testCases, resultStream, cfg.OsWrapper) | |
| close(resultStream) | |
| }() | |
| results, err := common.StreamResults(ctx, | |
| c.flags.Colors, | |
| c.flags.FailuresOnly, | |
| c.state, | |
| c.flags.Verbose, | |
| /* coverage */ nil, | |
| len(testCases), | |
| resultStream, | |
| cfg.OsWrapper) | |
| if err != nil { | |
| return err | |
| } | |
| if err := c.state.Close(results); err != nil { | |
| return err | |
| } | |
| return nil | |
| } | |
| // TODO(crbug.com/416755658): Add unittest coverage once there is a way to fake | |
| // the Chrome instance. | |
| // runTestCases spawns c.flags.NumRunners Chrome 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) runTestCases(ctx context.Context, testCases []common.TestCase, results chan<- common.Result, fsReader oswrapper.FilesystemReader) { | |
| // 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.runChromeInstance(ctx, id, testCases, testCaseIndices, results, fsReader); err != nil { | |
| results <- common.Result{ | |
| Status: common.Fail, | |
| Error: fmt.Errorf("Test server error: %w", err), | |
| } | |
| } | |
| }() | |
| } | |
| wg.Wait() | |
| } | |
| // TODO(crbug.com/416755658): Add unittest coverage once there is a way to fake | |
| // the Chrome instance. | |
| // runChromeInstance starts a Chrome 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 Chrome instance is shutdown and runChromeInstance returns. | |
| func (c *cmd) runChromeInstance( | |
| ctx context.Context, | |
| id int, | |
| testCases []common.TestCase, | |
| testCaseIndices <-chan int, | |
| results chan<- common.Result, | |
| fsReader oswrapper.FilesystemReader) error { | |
| port := 8800 + id | |
| type Request struct { | |
| Query string `json:"q"` | |
| UseWorker bool `json:"w"` | |
| } | |
| type Response struct { | |
| Type string | |
| Message string | |
| Log string `json:"log"` | |
| Status string | |
| Duration float64 `json:"js_duration_ms"` | |
| } | |
| requests := make(chan Request, 64) | |
| responses := make(chan Response, 64) | |
| errs := make(chan error, 64) | |
| handler := http.NewServeMux() | |
| handler.HandleFunc("/test_page.html", serveFile("webgpu-cts/test_page.html", fsReader)) | |
| handler.HandleFunc("/test_runner.js", serveFile("webgpu-cts/test_runner.js", fsReader)) | |
| handler.HandleFunc("/third_party/webgpu-cts/resources/", | |
| serveDir("/third_party/webgpu-cts/resources/", c.flags.CTS+"/out/resources/", fsReader)) | |
| handler.HandleFunc("/third_party/webgpu-cts/src/", | |
| serveDir("/third_party/webgpu-cts/src/", c.flags.CTS+"/out/", fsReader)) | |
| handler.HandleFunc("/", websocket.Handler(func(ws *websocket.Conn) { | |
| go func() { | |
| d := json.NewDecoder(ws) | |
| for { | |
| response := Response{} | |
| if err := d.Decode(&response); err == nil { | |
| responses <- response | |
| } else { | |
| errs <- err | |
| return | |
| } | |
| } | |
| }() | |
| e := json.NewEncoder(ws) | |
| for request := range requests { | |
| if err := e.Encode(request); err != nil { | |
| errs <- err | |
| return | |
| } | |
| } | |
| }).ServeHTTP) | |
| server := &http.Server{Addr: fmt.Sprint(":", port), Handler: handler} | |
| ln, err := net.Listen("tcp", server.Addr) | |
| if err != nil { | |
| return err | |
| } | |
| go func() { | |
| if c.flags.Verbose { | |
| fmt.Fprintf(c.state.Stdout, "[%v] listening on port %v\n", id, port) | |
| } | |
| if err := server.Serve(ln); err != nil { | |
| errs <- err | |
| } | |
| }() | |
| origin := fmt.Sprintf("http://localhost:%v", port) | |
| execOpts := []chromedp.ExecAllocatorOption{ | |
| chromedp.NoDefaultBrowserCheck, | |
| chromedp.NoFirstRun, | |
| chromedp.Flag("unsafely-treat-insecure-origin-as-secure", origin), | |
| chromedp.Flag("enable-unsafe-webgpu", true), | |
| chromedp.Flag("enable-features", "Vulkan,UseSkiaRenderer"), | |
| } | |
| if c.flags.chrome != "" { | |
| execOpts = append(execOpts, chromedp.ExecPath(c.flags.chrome)) | |
| } | |
| allocCtx, cancel := chromedp.NewExecAllocator(context.Background(), execOpts...) | |
| defer cancel() | |
| runCtx, cancel := chromedp.NewContext(allocCtx, | |
| chromedp.WithLogf(log.Printf), | |
| chromedp.WithErrorf(log.Printf)) | |
| defer cancel() | |
| if c.flags.Verbose { | |
| chromedp.ListenTarget(runCtx, func(ev interface{}) { | |
| switch ev := ev.(type) { | |
| case *runtime.EventConsoleAPICalled: | |
| args := make([]string, len(ev.Args)) | |
| for i := range ev.Args { | |
| args[i] = string(ev.Args[i].Value) | |
| } | |
| log.Println(ev.Type, strings.Join(args, " ")) | |
| case *runtime.EventExceptionThrown: | |
| log.Println(ev.ExceptionDetails.Error()) | |
| } | |
| }) | |
| } | |
| if err := chromedp.Run(runCtx, | |
| chromedp.Navigate(origin+"/test_page.html"), | |
| chromedp.Evaluate(fmt.Sprintf("window.setupWebsocket(%v);", port), | |
| nil)); err != nil { | |
| return err | |
| } | |
| nextTestCase: | |
| for idx := range testCaseIndices { | |
| res := common.Result{Index: idx, TestCase: testCases[idx]} | |
| if c.flags.Verbose { | |
| fmt.Println("Starting", res.TestCase) | |
| } | |
| requests <- Request{Query: string(res.TestCase)} | |
| for { | |
| // Future enhancements: Timeouts, browser restarting. | |
| select { | |
| case response := <-responses: | |
| switch response.Type { | |
| case "CONNECTION_ACK": | |
| if c.flags.Verbose { | |
| fmt.Fprintf(c.state.Stdout, "[%v] connected\n", id) | |
| } | |
| case "TEST_STARTED": | |
| case "TEST_HEARTBEAT": | |
| case "TEST_STATUS": | |
| res.Status = common.Status(response.Status) | |
| res.Duration = time.Duration(response.Duration) * time.Millisecond | |
| case "TEST_FINISHED": | |
| results <- res | |
| continue nextTestCase | |
| case "TEST_LOG": | |
| res.Message += response.Log | |
| default: | |
| return fmt.Errorf("unknown response type: %v", response.Type) | |
| } | |
| case err := <-errs: | |
| return err | |
| } | |
| } | |
| } | |
| if err := server.Shutdown(ctx); err != nil { | |
| return err | |
| } | |
| return nil | |
| } | |
| func serveFile(relPath string, fsReader oswrapper.FilesystemReader) func(http.ResponseWriter, *http.Request) { | |
| dawnRoot := fileutils.DawnRoot(fsReader) | |
| return func(w http.ResponseWriter, r *http.Request) { | |
| fullPath := filepath.Join(dawnRoot, relPath) | |
| if !fileutils.IsFile(fullPath, fsReader) { | |
| log.Printf("'%v' file does not exist", fullPath) | |
| } | |
| http.ServeFile(w, r, filepath.Join(dawnRoot, relPath)) | |
| } | |
| } | |
| func serveDir(remote, local string, fsReader oswrapper.FilesystemReader) func(http.ResponseWriter, *http.Request) { | |
| return func(w http.ResponseWriter, r *http.Request) { | |
| fullPath := filepath.Join(local, strings.TrimPrefix(r.URL.Path, remote)) | |
| if !fileutils.IsFile(fullPath, fsReader) { | |
| log.Printf("'%v' file does not exist", fullPath) | |
| } | |
| http.ServeFile(w, r, fullPath) | |
| } | |
| } |