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.Int64("buildbucket_build_id", 0, "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.Int64("issue", 0, "issue (if tryjob)") 48 patchset = flag.Int64("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 BuildBucketID: *buildBucketID, 180 Builder: *builder, 181 GitHash: *gitHash, 182 Issue: *issue, 183 Key: defaultKeys, 184 Patchset: *patchset, 185 Results: results, 186 TaskID: *taskId, 187 } 188 enc := json.NewEncoder(outputFile) 189 enc.SetIndent("", " ") // Make it human readable. 190 if err := enc.Encode(&dmresults); err != nil { 191 fmt.Println(err) 192 http.Error(w, "Could not write json to disk", 500) 193 return 194 } 195 fmt.Println("JSON Written") 196} 197 198// writeBase64EncodedPNG writes a PNG to disk and returns the md5 of the 199// decoded PNG bytes and any error. This hash is what will be used as 200// the gold digest and the file name. 201func writeBase64EncodedPNG(data string) (string, error) { 202 // data starts with something like data:image/png;base64,[data] 203 // https://en.wikipedia.org/wiki/Data_URI_scheme 204 start := strings.Index(data, ",") 205 b := bytes.NewBufferString(data[start+1:]) 206 pngReader := base64.NewDecoder(base64.StdEncoding, b) 207 208 pngBytes, err := ioutil.ReadAll(pngReader) 209 if err != nil { 210 return "", fmt.Errorf("Could not decode base 64 encoding %s", err) 211 } 212 213 // compute the hash of the pixel values, like DM does 214 img, err := png.Decode(bytes.NewBuffer(pngBytes)) 215 if err != nil { 216 return "", fmt.Errorf("Not a valid png: %s", err) 217 } 218 hash := "" 219 switch img.(type) { 220 case *image.NRGBA: 221 i := img.(*image.NRGBA) 222 hash = fmt.Sprintf("%x", md5.Sum(i.Pix)) 223 case *image.RGBA: 224 i := img.(*image.RGBA) 225 hash = fmt.Sprintf("%x", md5.Sum(i.Pix)) 226 case *image.RGBA64: 227 i := img.(*image.RGBA64) 228 hash = fmt.Sprintf("%x", md5.Sum(i.Pix)) 229 default: 230 return "", fmt.Errorf("Unknown type of image") 231 } 232 233 p := path.Join(*outDir, hash+".png") 234 outputFile, err := createOutputFile(p) 235 defer outputFile.Close() 236 if err != nil { 237 return "", fmt.Errorf("Could not create png file %s: %s", p, err) 238 } 239 if _, err = outputFile.Write(pngBytes); err != nil { 240 return "", fmt.Errorf("Could not write to file %s: %s", p, err) 241 } 242 return hash, nil 243} 244