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