• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1// Copyright 2018 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
7// This server runs along side the karma tests and listens for POST requests
8// when any test case reports it has output for Gold. See testReporter.js
9// for the browser side part.
10
11import (
12	"bytes"
13	"crypto/md5"
14	"encoding/base64"
15	"encoding/json"
16	"flag"
17	"fmt"
18	"image"
19	"image/png"
20	"io/ioutil"
21	"log"
22	"net/http"
23	"os"
24	"path"
25	"strings"
26	"sync"
27
28	"go.skia.org/infra/golden/go/jsonio"
29	"go.skia.org/infra/golden/go/types"
30)
31
32// This allows us to use upload_dm_results.py out of the box
33const JSON_FILENAME = "dm.json"
34
35var (
36	outDir = flag.String("out_dir", "/OUT/", "location to dump the Gold JSON and pngs")
37	port   = flag.String("port", "8081", "Port to listen on.")
38
39	botId            = flag.String("bot_id", "", "swarming bot id (deprecated/unused)")
40	browser          = flag.String("browser", "Chrome", "Browser Key")
41	buildBucketID    = flag.String("buildbucket_build_id", "", "Buildbucket build id key")
42	builder          = flag.String("builder", "", "Builder, like 'Test-Debian9-EMCC-GCE-CPU-AVX2-wasm-Debug-All-PathKit'")
43	compiledLanguage = flag.String("compiled_language", "wasm", "wasm or asm.js")
44	config           = flag.String("config", "Release", "Configuration (e.g. Debug/Release) key")
45	gitHash          = flag.String("git_hash", "-", "The git commit hash of the version being tested")
46	hostOS           = flag.String("host_os", "Debian9", "OS Key")
47	issue            = flag.String("issue", "", "ChangeListID (if tryjob)")
48	patchset         = flag.Int("patchset", 0, "patchset (if tryjob)")
49	taskId           = flag.String("task_id", "", "swarming task id")
50	sourceType       = flag.String("source_type", "pathkit", "Gold Source type, like pathkit,canvaskit")
51)
52
53// Received from the JS side.
54type reportBody struct {
55	// e.g. "canvas" or "svg"
56	OutputType string `json:"output_type"`
57	// a base64 encoded PNG image.
58	Data string `json:"data"`
59	// a name describing the test. Should be unique enough to allow use of grep.
60	TestName string `json:"test_name"`
61}
62
63// The keys to be used at the top level for all Results.
64var defaultKeys map[string]string
65
66// contains all the results reported in through report_gold_data
67var results []*jsonio.Result
68var resultsMutex sync.Mutex
69
70func main() {
71	flag.Parse()
72
73	cpuGPU := "CPU"
74	if strings.Index(*builder, "-GPU-") != -1 {
75		cpuGPU = "GPU"
76	}
77	defaultKeys = map[string]string{
78		"arch":              "WASM",
79		"browser":           *browser,
80		"compiled_language": *compiledLanguage,
81		"compiler":          "emsdk",
82		"configuration":     *config,
83		"cpu_or_gpu":        cpuGPU,
84		"cpu_or_gpu_value":  "Browser",
85		"os":                *hostOS,
86		"source_type":       *sourceType,
87	}
88
89	results = []*jsonio.Result{}
90
91	http.HandleFunc("/report_gold_data", reporter)
92	http.HandleFunc("/dump_json", dumpJSON)
93
94	fmt.Printf("Waiting for gold ingestion on port %s\n", *port)
95
96	log.Fatal(http.ListenAndServe(":"+*port, nil))
97}
98
99// reporter handles when the client reports a test has Gold output.
100// It writes the corresponding PNG to disk and appends a Result, assuming
101// no errors.
102func reporter(w http.ResponseWriter, r *http.Request) {
103	if r.Method != "POST" {
104		http.Error(w, "Only POST accepted", 400)
105		return
106	}
107	defer r.Body.Close()
108
109	body, err := ioutil.ReadAll(r.Body)
110	if err != nil {
111		http.Error(w, "Malformed body", 400)
112		return
113	}
114
115	testOutput := reportBody{}
116	if err := json.Unmarshal(body, &testOutput); err != nil {
117		fmt.Println(err)
118		http.Error(w, "Could not unmarshal JSON", 400)
119		return
120	}
121
122	hash := ""
123	if hash, err = writeBase64EncodedPNG(testOutput.Data); err != nil {
124		fmt.Println(err)
125		http.Error(w, "Could not write image to disk", 500)
126		return
127	}
128
129	if _, err := w.Write([]byte("Accepted")); err != nil {
130		fmt.Printf("Could not write response: %s\n", err)
131		return
132	}
133
134	resultsMutex.Lock()
135	defer resultsMutex.Unlock()
136	results = append(results, &jsonio.Result{
137		Digest: types.Digest(hash),
138		Key: map[string]string{
139			"name":   testOutput.TestName,
140			"config": testOutput.OutputType,
141		},
142		Options: map[string]string{
143			"ext": "png",
144		},
145	})
146}
147
148// createOutputFile creates a file and set permissions correctly.
149func createOutputFile(p string) (*os.File, error) {
150	outputFile, err := os.Create(p)
151	if err != nil {
152		return nil, fmt.Errorf("Could not open file %s on disk: %s", p, err)
153	}
154	// Make this accessible (and deletable) by all users
155	if err = outputFile.Chmod(0666); err != nil {
156		return nil, fmt.Errorf("Could not change permissions of file %s: %s", p, err)
157	}
158	return outputFile, nil
159}
160
161// dumpJSON writes out a JSON file with all the results, typically at the end of
162// all the tests.
163func dumpJSON(w http.ResponseWriter, r *http.Request) {
164	if r.Method != "POST" {
165		http.Error(w, "Only POST accepted", 400)
166		return
167	}
168
169	p := path.Join(*outDir, JSON_FILENAME)
170	outputFile, err := createOutputFile(p)
171	defer outputFile.Close()
172	if err != nil {
173		fmt.Println(err)
174		http.Error(w, "Could not open json file on disk", 500)
175		return
176	}
177
178	dmresults := jsonio.GoldResults{
179		Builder:                     *builder,
180		ChangeListID:                *issue,
181		CodeReviewSystem:            "gerrit",
182		ContinuousIntegrationSystem: "buildbucket",
183		GitHash:                     *gitHash,
184		Key:                         defaultKeys,
185		PatchSetOrder:               *patchset,
186		Results:                     results,
187		TaskID:                      *taskId,
188		TryJobID:                    *buildBucketID,
189	}
190	enc := json.NewEncoder(outputFile)
191	enc.SetIndent("", "  ") // Make it human readable.
192	if err := enc.Encode(&dmresults); err != nil {
193		fmt.Println(err)
194		http.Error(w, "Could not write json to disk", 500)
195		return
196	}
197	fmt.Println("JSON Written")
198}
199
200// writeBase64EncodedPNG writes a PNG to disk and returns the md5 of the
201// decoded PNG bytes and any error. This hash is what will be used as
202// the gold digest and the file name.
203func writeBase64EncodedPNG(data string) (string, error) {
204	// data starts with something like data:image/png;base64,[data]
205	// https://en.wikipedia.org/wiki/Data_URI_scheme
206	start := strings.Index(data, ",")
207	b := bytes.NewBufferString(data[start+1:])
208	pngReader := base64.NewDecoder(base64.StdEncoding, b)
209
210	pngBytes, err := ioutil.ReadAll(pngReader)
211	if err != nil {
212		return "", fmt.Errorf("Could not decode base 64 encoding %s", err)
213	}
214
215	// compute the hash of the pixel values, like DM does
216	img, err := png.Decode(bytes.NewBuffer(pngBytes))
217	if err != nil {
218		return "", fmt.Errorf("Not a valid png: %s", err)
219	}
220	hash := ""
221	switch img.(type) {
222	case *image.NRGBA:
223		i := img.(*image.NRGBA)
224		hash = fmt.Sprintf("%x", md5.Sum(i.Pix))
225	case *image.RGBA:
226		i := img.(*image.RGBA)
227		hash = fmt.Sprintf("%x", md5.Sum(i.Pix))
228	case *image.RGBA64:
229		i := img.(*image.RGBA64)
230		hash = fmt.Sprintf("%x", md5.Sum(i.Pix))
231	default:
232		return "", fmt.Errorf("Unknown type of image")
233	}
234
235	p := path.Join(*outDir, hash+".png")
236	outputFile, err := createOutputFile(p)
237	defer outputFile.Close()
238	if err != nil {
239		return "", fmt.Errorf("Could not create png file %s: %s", p, err)
240	}
241	if _, err = outputFile.Write(pngBytes); err != nil {
242		return "", fmt.Errorf("Could not write to file %s: %s", p, err)
243	}
244	return hash, nil
245}
246