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