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