1// Copyright 2022 The Chromium Authors. All rights reserved. 2// Use of this source code is governed by a BSD-style license that can be 3// found in the LICENSE file. 4 5package main 6 7import ( 8 "encoding/base64" 9 "encoding/json" 10 "errors" 11 "fmt" 12 "io" 13 "io/ioutil" 14 "net" 15 "net/http" 16 "os" 17 "os/signal" 18 "path" 19 "path/filepath" 20 "strconv" 21 "syscall" 22) 23 24const ( 25 envPortFileBaseName = "port" 26) 27 28func main() { 29 envDir, envReadyFile := mustGetEnvironmentVariables() 30 31 port, listener := mustGetUnusedNetworkPort() 32 33 beginTestManagementLogic(listener) 34 35 mustPrepareTestEnvironment(envDir, port) 36 37 setupTerminationLogic() 38 39 mustSignalTestsCanBegin(envReadyFile) 40 41 select {} // Block until the termination handler calls os.Exit 42} 43 44// mustGetEnvironmentVariables returns two file paths: a directory that can be used to communicate 45// between this binary and the test binaries, and the file that needs to be created when this 46// binary has finished setting things up. It panics if it cannot read the values from the 47// set environment variables. 48func mustGetEnvironmentVariables() (string, string) { 49 // Read in build paths to the ready and port files. 50 envDir := os.Getenv("ENV_DIR") 51 if envDir == "" { 52 panic("required environment variable ENV_DIR is unset") 53 } 54 envReadyFile := os.Getenv("ENV_READY_FILE") 55 if envReadyFile == "" { 56 panic("required environment variable ENV_READY_FILE is unset") 57 } 58 return envDir, envReadyFile 59} 60 61// mustGetUnusedNetworkPort returns a network port chosen by the OS (and assumed to be previously 62// unused) and a listener for that port. We choose a non-deterministic port instead of a fixed port 63// because multiple tests may be running in parallel. 64func mustGetUnusedNetworkPort() (int, net.Listener) { 65 // Listen on an unused port chosen by the OS. 66 listener, err := net.Listen("tcp", ":0") 67 if err != nil { 68 panic(err) 69 } 70 port := listener.Addr().(*net.TCPAddr).Port 71 fmt.Printf("Environment is ready to go!\nListening on port %d.\n", port) 72 return port, listener 73} 74 75// beginTestManagementLogic sets up the server endpoints which allow the JS gm() tests to exfiltrate 76// their PNG images by means of a POST request. 77func beginTestManagementLogic(listener net.Listener) { 78 // The contents of this path go to //bazel-testlogs/path/to/test/test.outputs/ and are combined 79 // into outputs.zip. 80 // e.g. ls bazel-testlogs/modules/canvaskit/hello_world_test_with_env/test.outputs/ 81 // test_001 82 // test_002 83 // outputs.zip # contains test_001 and test_002 84 // This environment var is documented in https://bazel.build/reference/test-encyclopedia 85 // Note that Bazel expects a zip executable to be present on this machine in order to do this. 86 // https://github.com/bazelbuild/bazel/blob/b9ffc16b94c1ee101031b0c010453847bdc532d1/tools/test/test-setup.sh#L425 87 outPath := os.Getenv("TEST_UNDECLARED_OUTPUTS_DIR") 88 if outPath == "" { 89 panic("output directory was not configured") 90 } 91 92 http.HandleFunc("/healthz", func(w http.ResponseWriter, r *http.Request) { 93 w.WriteHeader(http.StatusOK) 94 }) 95 96 http.HandleFunc("/report", func(w http.ResponseWriter, r *http.Request) { 97 payload, err := readPayload(r) 98 if err != nil { 99 http.Error(w, err.Error(), http.StatusBadRequest) 100 } 101 if payload.TestName == "" { 102 http.Error(w, "Must specify test name", http.StatusBadRequest) 103 return 104 } 105 // Write the data in the POST to the special Bazel output directory 106 fileContents, err := base64.StdEncoding.DecodeString(payload.Base64Data) 107 if err != nil { 108 fmt.Printf("Invalid base64 data: %s\n", err.Error()) 109 http.Error(w, "Invalid base64 data "+err.Error(), http.StatusBadRequest) 110 return 111 } 112 fileName := payload.TestName 113 if payload.Config != "" { 114 fileName += "." + payload.Config 115 } 116 117 fp := filepath.Join(outPath, fileName+".png") 118 // Two newlines here makes the log stick out more. 119 fmt.Printf("Writing test data to %s\n\n", fp) 120 out, err := os.Create(fp) 121 if err != nil { 122 http.Error(w, err.Error(), http.StatusInternalServerError) 123 panic(err) 124 } 125 if _, err := out.Write(fileContents); err != nil { 126 http.Error(w, err.Error(), http.StatusInternalServerError) 127 panic(err) 128 } 129 130 // Signal to the test that we have written the data to disk. Tests should be sure to wait 131 // for this response before signaling they are done to avoid a race condition. 132 w.WriteHeader(http.StatusCreated) 133 // We are not worried about an XSS reflection attack here on a local server only up 134 // when running tests. 135 if _, err := fmt.Fprintln(w, "Accepted for test "+payload.TestName); err != nil { 136 panic(err) 137 } 138 }) 139 go func() { 140 serveForever(listener) 141 }() 142} 143 144type testPayload struct { 145 TestName string `json:"name"` 146 Base64Data string `json:"b64_data"` 147 // Config, if set, will be added as a suffix before the .png in the file name 148 // e.g. test_name.html_canvas.png. This will be parsed before uploading to Gold to be used 149 // as the value for the "config" key. This allows us to have different variants of the same 150 // test to compare and contrast. 151 Config string `json:"config"` 152} 153 154// readPayload reads the body of the given request as JSON and parses it into a testPayload struct. 155func readPayload(r *http.Request) (testPayload, error) { 156 var payload testPayload 157 if r.Body == nil { 158 return payload, errors.New("no body received") 159 } 160 b, err := io.ReadAll(r.Body) 161 if err != nil { 162 return payload, err 163 } 164 _ = r.Body.Close() 165 if err := json.Unmarshal(b, &payload); err != nil { 166 return payload, errors.New("invalid JSON") 167 } 168 return payload, nil 169} 170 171// serveForever serves the given listener and blocks. If it could not start serving, it will panic. 172func serveForever(listener net.Listener) { 173 // If http.Serve returns, it is an error. 174 if err := http.Serve(listener, nil); err != nil { 175 panic(fmt.Sprintf("Finished serving due to error: %s\n", err)) 176 } 177} 178 179// mustPrepareTestEnvironment writes any files to the temporary test directory. This is just a file 180// that indicates which port the gold tests should make POST requests to. It panics if there are 181// any errors. 182func mustPrepareTestEnvironment(dirTestsCanRead string, port int) { 183 envPortFile := path.Join(dirTestsCanRead, envPortFileBaseName) 184 if err := ioutil.WriteFile(envPortFile, []byte(strconv.Itoa(port)), 0644); err != nil { 185 panic(err) 186 } 187} 188 189// setupTerminationLogic creates a handler for SIGTERM which is what test_on_env will send the 190// environment when the tests complete. There is currently nothing to do other than exit. 191func setupTerminationLogic() { 192 c := make(chan os.Signal, 1) 193 go func() { 194 <-c 195 os.Exit(0) 196 }() 197 signal.Notify(c, syscall.SIGTERM) 198} 199 200// mustSignalTestsCanBegin creates the agreed upon ENV_READY_FILE which signals the test binary can 201// be executed by Bazel. See test_on_env.bzl for more. It panics if the file cannot be created. 202func mustSignalTestsCanBegin(envReadyFile string) { 203 if err := ioutil.WriteFile(envReadyFile, []byte{}, 0644); err != nil { 204 panic(err) 205 } 206} 207