• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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