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.
25 envPortFileBaseName = "port"
29 envDir, envReadyFile := mustGetEnvironmentVariables()
31 port, listener := mustGetUnusedNetworkPort()
33 beginTestManagementLogic(listener)
35 mustPrepareTestEnvironment(envDir, port)
37 setupTerminationLogic()
39 mustSignalTestsCanBegin(envReadyFile)
41 select {} // Block until the termination handler calls os.Exit
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.
48 func mustGetEnvironmentVariables() (string, string) {
49 // Read in build paths to the ready and port files.
50 envDir := os.Getenv("ENV_DIR")
52 panic("required environment variable ENV_DIR is unset")
54 envReadyFile := os.Getenv("ENV_READY_FILE")
55 if envReadyFile == "" {
56 panic("required environment variable ENV_READY_FILE is unset")
58 return envDir, envReadyFile
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.
64 func mustGetUnusedNetworkPort() (int, net.Listener) {
65 // Listen on an unused port chosen by the OS.
66 listener, err := net.Listen("tcp", ":0")
70 port := listener.Addr().(*net.TCPAddr).Port
71 fmt.Printf("Environment is ready to go!\nListening on port %d.\n", port)
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.
77 func beginTestManagementLogic(listener net.Listener) {
78 // The contents of this path go to //bazel-testlogs/path/to/test/test.outputs/ and are combined
80 // e.g. ls bazel-testlogs/modules/canvaskit/hello_world_test_with_env/test.outputs/
83 // outputs.zip # contains test_001 and test_002
84 // This environment var is documented in https://bazel.build/reference/test-encyclopedia
85 outPath := os.Getenv("TEST_UNDECLARED_OUTPUTS_DIR")
87 panic("output directory was not configured")
90 http.HandleFunc("/healthz", func(w http.ResponseWriter, r *http.Request) {
91 w.WriteHeader(http.StatusOK)
94 http.HandleFunc("/report", func(w http.ResponseWriter, r *http.Request) {
95 payload, err := readPayload(r)
97 http.Error(w, err.Error(), http.StatusBadRequest)
99 if payload.TestName == "" {
100 http.Error(w, "Must specify test name", http.StatusBadRequest)
103 // Write the data in the POST to the special Bazel output directory
104 fileContents, err := base64.StdEncoding.DecodeString(payload.Base64Data)
106 fmt.Printf("Invalid base64 data: %s\n", err.Error())
107 http.Error(w, "Invalid base64 data "+err.Error(), http.StatusBadRequest)
110 fp := filepath.Join(outPath, payload.TestName)
111 // Two newlines here makes the log stick out more.
112 fmt.Printf("Writing test data to %s\n\n", fp)
113 out, err := os.Create(fp)
115 http.Error(w, err.Error(), http.StatusInternalServerError)
118 if _, err := out.Write(fileContents); err != nil {
119 http.Error(w, err.Error(), http.StatusInternalServerError)
123 // Signal to the test that we have written the data to disk. Tests should be sure to wait
124 // for this response before signaling they are done to avoid a race condition.
125 w.WriteHeader(http.StatusCreated)
126 // We are not worried about an XSS reflection attack here on a local server only up
127 // when running tests.
128 if _, err := fmt.Fprintln(w, "Accepted for test "+payload.TestName); err != nil {
133 serveForever(listener)
137 type testPayload struct {
138 TestName string `json:"name"`
139 Base64Data string `json:"b64_data"`
142 // readPayload reads the body of the given request as JSON and parses it into a testPayload struct.
143 func readPayload(r *http.Request) (testPayload, error) {
144 var payload testPayload
146 return payload, errors.New("no body received")
148 b, err := io.ReadAll(r.Body)
153 if err := json.Unmarshal(b, &payload); err != nil {
154 return payload, errors.New("invalid JSON")
159 // serveForever serves the given listener and blocks. If it could not start serving, it will panic.
160 func serveForever(listener net.Listener) {
161 // If http.Serve returns, it is an error.
162 if err := http.Serve(listener, nil); err != nil {
163 panic(fmt.Sprintf("Finished serving due to error: %s\n", err))
167 // mustPrepareTestEnvironment writes any files to the temporary test directory. This is just a file
168 // that indicates which port the gold tests should make POST requests to. It panics if there are
170 func mustPrepareTestEnvironment(dirTestsCanRead string, port int) {
171 envPortFile := path.Join(dirTestsCanRead, envPortFileBaseName)
172 if err := ioutil.WriteFile(envPortFile, []byte(strconv.Itoa(port)), 0644); err != nil {
177 // setupTerminationLogic creates a handler for SIGTERM which is what test_on_env will send the
178 // environment when the tests complete. There is currently nothing to do other than exit.
179 func setupTerminationLogic() {
180 c := make(chan os.Signal, 1)
185 signal.Notify(c, syscall.SIGTERM)
188 // mustSignalTestsCanBegin creates the agreed upon ENV_READY_FILE which signals the test binary can
189 // be executed by Bazel. See test_on_env.bzl for more. It panics if the file cannot be created.
190 func mustSignalTestsCanBegin(envReadyFile string) {
191 if err := ioutil.WriteFile(envReadyFile, []byte{}, 0644); err != nil {