// Copyright 2018 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. package main // This server runs alongside lottiecap.js and istens for POST requests // when any test case reports it has output for Gold. // TODO(kjlubick): Deduplicate with pathkit-aggregator // TODO(kjlubick): Handle uninteresting_hash.txt if needed. import ( "bytes" "crypto/md5" "encoding/base64" "encoding/json" "flag" "fmt" "image" "image/png" "io/ioutil" "log" "net/http" "os" "path" "strings" "go.skia.org/infra/golden/go/jsonio" "go.skia.org/infra/golden/go/types" ) // This allows us to use upload_dm_results.py out of the box const JSON_FILENAME = "dm.json" var ( outDir = flag.String("out_dir", "/OUT/", "location to dump the Gold JSON and pngs") port = flag.String("port", "8081", "Port to listen on.") browser = flag.String("browser", "Chrome", "Browser Key") buildBucketID = flag.String("buildbucket_build_id", "", "Buildbucket build id key") builder = flag.String("builder", "", "Builder, like 'Test-Debian9-EMCC-GCE-CPU-AVX2-wasm-Debug-All-PathKit'") renderer = flag.String("renderer", "lottie-web", "e.g. lottie-web or skottie") config = flag.String("config", "Release", "Configuration (e.g. Debug/Release) key") gitHash = flag.String("git_hash", "-", "The git commit hash of the version being tested") hostOS = flag.String("host_os", "Debian9", "OS Key") issue = flag.String("issue", "", "ChangeListID (if tryjob)") patchset = flag.Int("patchset", 0, "patchset (if tryjob)") taskId = flag.String("task_id", "", "Skia task id") ) // reportBody is the JSON recieved from the JS side. It represents // exactly one unique Gold image/test. type reportBody struct { // a base64 encoded PNG image. Data string `json:"data"` // a name describing the test. Should be unique enough to allow use of grep. TestName string `json:"test_name"` } // The keys to be used at the top level for all Results. var defaultKeys map[string]string // contains all the results reported in through report_gold_data var results []*jsonio.Result func main() { flag.Parse() defaultKeys = map[string]string{ "browser": *browser, "renderer": *renderer, "configuration": *config, "cpu_or_gpu": "CPU", "cpu_or_gpu_value": "Browser", "os": *hostOS, "source_type": "lottie", } results = []*jsonio.Result{} http.HandleFunc("/report_gold_data", reporter) http.HandleFunc("/dump_json", dumpJSON) fmt.Printf("Waiting for gold ingestion on port %s\n", *port) log.Fatal(http.ListenAndServe(":"+*port, nil)) } // reporter handles when the client reports a test has Gold output. // It writes the corresponding PNG to disk and appends a Result, assuming // no errors. func reporter(w http.ResponseWriter, r *http.Request) { if r.Method != "POST" { http.Error(w, "Only POST accepted", 400) return } defer r.Body.Close() body, err := ioutil.ReadAll(r.Body) if err != nil { http.Error(w, "Malformed body", 400) return } testOutput := reportBody{} if err := json.Unmarshal(body, &testOutput); err != nil { fmt.Println(err) http.Error(w, "Could not unmarshal JSON", 400) return } hash := "" if hash, err = writeBase64EncodedPNG(testOutput.Data); err != nil { fmt.Println(err) http.Error(w, "Could not write image to disk", 500) return } if _, err := w.Write([]byte("Accepted")); err != nil { fmt.Printf("Could not write response: %s\n", err) return } results = append(results, &jsonio.Result{ Digest: types.Digest(hash), Key: map[string]string{ "name": testOutput.TestName, }, Options: map[string]string{ "ext": "png", }, }) } // createOutputFile creates a file and set permissions correctly. func createOutputFile(p string) (*os.File, error) { outputFile, err := os.Create(p) if err != nil { return nil, fmt.Errorf("Could not open file %s on disk: %s", p, err) } // Make this accessible (and deletable) by all users if err = outputFile.Chmod(0666); err != nil { return nil, fmt.Errorf("Could not change permissions of file %s: %s", p, err) } return outputFile, nil } // dumpJSON writes out a JSON file with all the results, typically at the end of // all the tests. func dumpJSON(w http.ResponseWriter, r *http.Request) { if r.Method != "POST" { http.Error(w, "Only POST accepted", 400) return } p := path.Join(*outDir, JSON_FILENAME) outputFile, err := createOutputFile(p) defer outputFile.Close() if err != nil { fmt.Println(err) http.Error(w, "Could not open json file on disk", 500) return } results := jsonio.GoldResults{ Builder: *builder, ChangeListID: *issue, GitHash: *gitHash, CodeReviewSystem: "gerrit", Key: defaultKeys, PatchSetOrder: *patchset, Results: results, TaskID: *taskId, TryJobID: *buildBucketID, ContinuousIntegrationSystem: "buildbucket", } enc := json.NewEncoder(outputFile) enc.SetIndent("", " ") // Make it human readable. if err := enc.Encode(&results); err != nil { fmt.Println(err) http.Error(w, "Could not write json to disk", 500) return } fmt.Println("JSON Written") } // writeBase64EncodedPNG writes a PNG to disk and returns the md5 of the // decoded PNG bytes and any error. This hash is what will be used as // the gold digest and the file name. func writeBase64EncodedPNG(data string) (string, error) { // data starts with something like data:image/png;base64,[data] // https://en.wikipedia.org/wiki/Data_URI_scheme start := strings.Index(data, ",") b := bytes.NewBufferString(data[start+1:]) pngReader := base64.NewDecoder(base64.StdEncoding, b) pngBytes, err := ioutil.ReadAll(pngReader) if err != nil { return "", fmt.Errorf("Could not decode base 64 encoding %s", err) } // compute the hash of the pixel values, like DM does img, err := png.Decode(bytes.NewBuffer(pngBytes)) if err != nil { return "", fmt.Errorf("Not a valid png: %s", err) } hash := "" switch img.(type) { case *image.NRGBA: i := img.(*image.NRGBA) hash = fmt.Sprintf("%x", md5.Sum(i.Pix)) case *image.RGBA: i := img.(*image.RGBA) hash = fmt.Sprintf("%x", md5.Sum(i.Pix)) case *image.RGBA64: i := img.(*image.RGBA64) hash = fmt.Sprintf("%x", md5.Sum(i.Pix)) default: return "", fmt.Errorf("Unknown type of image") } p := path.Join(*outDir, hash+".png") outputFile, err := createOutputFile(p) defer outputFile.Close() if err != nil { return "", fmt.Errorf("Could not create png file %s: %s", p, err) } if _, err = outputFile.Write(pngBytes); err != nil { return "", fmt.Errorf("Could not write to file %s: %s", p, err) } return hash, nil }