|  | package goldclient | 
|  |  | 
|  | import ( | 
|  | "bufio" | 
|  | "bytes" | 
|  | "context" | 
|  | "encoding/json" | 
|  | "fmt" | 
|  | "net/url" | 
|  | "regexp" | 
|  |  | 
|  | "go.skia.org/infra/go/now" | 
|  | "go.skia.org/infra/go/skerr" | 
|  | "go.skia.org/infra/golden/go/expectations" | 
|  | "go.skia.org/infra/golden/go/jsonio" | 
|  | "go.skia.org/infra/golden/go/types" | 
|  | "go.skia.org/infra/golden/go/web/frontend" | 
|  | ) | 
|  |  | 
|  | const ( | 
|  | // jsonPrefix is the path prefix in the GCS bucket that holds JSON result files | 
|  | jsonPrefix = "dm-json-v1" | 
|  |  | 
|  | // imagePrefix is the path prefix in the GCS bucket that holds images. | 
|  | imagePrefix = "dm-images-v1" | 
|  |  | 
|  | // goldHostTemplate constructs the URL of the Gold instance from the instance id | 
|  | goldHostTemplate = "https://%s-gold.skia.org" | 
|  |  | 
|  | // bucketTemplate constructs the name of the ingestion bucket from the instance id | 
|  | bucketTemplate = "skia-gold-%s" | 
|  |  | 
|  | hostFuchsiaCorp   = "https://fuchsia-gold.corp.goog" | 
|  | instanceIDFuchsia = "fuchsia" | 
|  | ) | 
|  |  | 
|  | // md5Regexp is used to check whether strings are MD5 hashes. | 
|  | var md5Regexp = regexp.MustCompile(`^[a-f0-9]{32}$`) | 
|  |  | 
|  | // resultState is an internal container for all information to upload results | 
|  | // to Gold, including the jsonio.GoldResult structure itself. | 
|  | type resultState struct { | 
|  | // SharedConfig is all the data that is common test to test, for example, the | 
|  | // keys about this machine (e.g. GPU, OS). | 
|  | SharedConfig    jsonio.GoldResults | 
|  | PerTestPassFail bool | 
|  | FailureFile     string | 
|  | UploadOnly      bool | 
|  | InstanceID      string | 
|  | GoldURL         string | 
|  | Bucket          string | 
|  | KnownHashes     types.DigestSet | 
|  | Expectations    expectations.Baseline | 
|  | } | 
|  |  | 
|  | // newResultState creates a new instance of resultState | 
|  | func newResultState(sharedConfig jsonio.GoldResults, config *GoldClientConfig) *resultState { | 
|  | goldURL := config.OverrideGoldURL | 
|  | if goldURL == "" { | 
|  | goldURL = getGoldInstanceURL(config.InstanceID) | 
|  | } | 
|  | bucket := config.OverrideBucket | 
|  | if bucket == "" { | 
|  | bucket = getBucket(config.InstanceID) | 
|  | } | 
|  |  | 
|  | ret := &resultState{ | 
|  | SharedConfig:    sharedConfig, | 
|  | PerTestPassFail: config.PassFailStep, | 
|  | FailureFile:     config.FailureFile, | 
|  | InstanceID:      config.InstanceID, | 
|  | UploadOnly:      config.UploadOnly, | 
|  | GoldURL:         goldURL, | 
|  | Bucket:          bucket, | 
|  | } | 
|  |  | 
|  | return ret | 
|  | } | 
|  |  | 
|  | // getGoldInstanceURL returns the URL for a given Gold instance id. | 
|  | // This is usually a formulaic transform, but there are some special cases. | 
|  | func getGoldInstanceURL(instanceID string) string { | 
|  | if instanceID == instanceIDFuchsia { | 
|  | return hostFuchsiaCorp | 
|  | } | 
|  | return fmt.Sprintf(goldHostTemplate, instanceID) | 
|  | } | 
|  |  | 
|  | // getBucket returns the formulaic bucket name for a given instance id. Legacy instances may | 
|  | // have a different naming scheme and would override the bucket. | 
|  | func getBucket(instanceID string) string { | 
|  | return fmt.Sprintf(bucketTemplate, instanceID) | 
|  | } | 
|  |  | 
|  | // loadKnownHashes loads the list of known hashes from the Gold instance. | 
|  | func (r *resultState) loadKnownHashes(ctx context.Context) error { | 
|  | r.KnownHashes = types.DigestSet{} | 
|  |  | 
|  | // Fetch the known hashes via http | 
|  | hashesURL := r.GoldURL + frontend.KnownHashesRouteV1 | 
|  | body, err := getWithRetries(ctx, hashesURL) | 
|  | if err != nil { | 
|  | return skerr.Wrapf(err, "getting known hashes from %s (with retries)", hashesURL) | 
|  | } | 
|  |  | 
|  | scanner := bufio.NewScanner(bytes.NewBuffer(body)) | 
|  | for scanner.Scan() { | 
|  | // Ignore empty lines and lines that are not valid MD5 hashes | 
|  | line := bytes.TrimSpace(scanner.Bytes()) | 
|  | if len(line) > 0 && md5Regexp.Match(line) { | 
|  | r.KnownHashes[types.Digest(line)] = true | 
|  | } | 
|  | } | 
|  | if err := scanner.Err(); err != nil { | 
|  | return skerr.Wrapf(err, "scanning response of HTTP request") | 
|  | } | 
|  | return nil | 
|  | } | 
|  |  | 
|  | // loadExpectations fetches the expectations from Gold to compare to tests. | 
|  | func (r *resultState) loadExpectations(ctx context.Context) error { | 
|  | urlPath := frontend.ExpectationsRouteV2 | 
|  | if r.SharedConfig.ChangelistID != "" { | 
|  | urlPath = fmt.Sprintf("%s?issue=%s&crs=%s", urlPath, url.QueryEscape(r.SharedConfig.ChangelistID), url.QueryEscape(r.SharedConfig.CodeReviewSystem)) | 
|  | } | 
|  |  | 
|  | u := r.GoldURL + urlPath | 
|  | jsonBytes, err := getWithRetries(ctx, u) | 
|  | if err != nil { | 
|  | return skerr.Wrapf(err, "getting expectations from %s (with retries)", u) | 
|  | } | 
|  |  | 
|  | exp := &frontend.BaselineV2Response{} | 
|  |  | 
|  | if err := json.Unmarshal(jsonBytes, exp); err != nil { | 
|  | infof(ctx, "Fetched from %s\n", u) | 
|  | if len(jsonBytes) > 200 { | 
|  | infof(ctx, `Invalid JSON: "%s..."`, string(jsonBytes[0:200])) | 
|  | } else { | 
|  | infof(ctx, `Invalid JSON: %q`, string(jsonBytes)) | 
|  | } | 
|  | return skerr.Wrapf(err, "parsing JSON; this sometimes means auth issues") | 
|  | } | 
|  | if len(exp.Expectations) == 0 { | 
|  | errorf(ctx, "warning: got empty expectations when querying %s\n", u) | 
|  | errorf(ctx, "raw expectation response %q\n", string(jsonBytes)) | 
|  | } | 
|  |  | 
|  | r.Expectations = exp.Expectations | 
|  | return nil | 
|  | } | 
|  |  | 
|  | // getResultFilePath returns that path in GCS where the result file should be stored. | 
|  | // | 
|  | // The path follows the path described here: | 
|  | // | 
|  | //	https://github.com/google/skia-buildbot/blob/master/golden/docs/INGESTION.md | 
|  | // | 
|  | // The file name of the path also contains a timestamp to make it unique since all | 
|  | // calls within the same test run are written to the same output path. | 
|  | func (r *resultState) getResultFilePath(ctx context.Context) string { | 
|  | ts := now.Now(ctx).UTC() | 
|  | year, month, day := ts.Date() | 
|  | hour := ts.Hour() | 
|  |  | 
|  | // Assemble a path that looks like this: | 
|  | // <path_prefix>/YYYY/MM/DD/HH/<git_hash_or_cl>/<job_id>/<per_run_file_name>.json | 
|  | // The first segments up to 'HH' are required so the Gold ingester can scan these prefixes for | 
|  | // new files. The later segments are necessary to make the path unique within the runs of one | 
|  | // hour and increase readability of the paths for troubleshooting. | 
|  | // It is vital that the time segments of the path are based on UTC location. | 
|  | fileName := fmt.Sprintf("dm-%d.json", ts.UnixNano()) | 
|  | jobID := r.SharedConfig.TryJobID | 
|  | if jobID == "" { | 
|  | jobID = "waterfall" | 
|  | } | 
|  | gitHashOrCL := r.SharedConfig.GitHash | 
|  | if r.SharedConfig.ChangelistID != "" { | 
|  | gitHashOrCL = fmt.Sprintf("%s_%s_%d", r.SharedConfig.ChangelistID, r.SharedConfig.PatchsetID, r.SharedConfig.PatchsetOrder) | 
|  | } else if gitHashOrCL == "" { | 
|  | gitHashOrCL = r.SharedConfig.CommitID | 
|  | } | 
|  | segments := []interface{}{ | 
|  | jsonPrefix, | 
|  | year, | 
|  | month, | 
|  | day, | 
|  | hour, | 
|  | gitHashOrCL, | 
|  | jobID, | 
|  | fileName} | 
|  | path := fmt.Sprintf("%s/%04d/%02d/%02d/%02d/%s/%s/%s", segments...) | 
|  |  | 
|  | if r.SharedConfig.ChangelistID != "" { | 
|  | path = "trybot/" + path | 
|  | } | 
|  | return fmt.Sprintf("%s/%s", r.Bucket, path) | 
|  | } | 
|  |  | 
|  | // getGCSImagePath returns the path in GCS where the image with the given hash should be stored. | 
|  | func (r *resultState) getGCSImagePath(imgHash types.Digest) string { | 
|  | return fmt.Sprintf("gs://%s/%s/%s.png", r.Bucket, imagePrefix, imgHash) | 
|  | } | 
|  |  | 
|  | // loadStateFromJSON loads a serialization of a resultState instance that was previously written | 
|  | // via the save method. | 
|  | func loadStateFromJSON(fileName string) (*resultState, error) { | 
|  | ret := &resultState{} | 
|  | exists, err := loadJSONFile(fileName, ret) | 
|  | if err != nil { | 
|  | return nil, err | 
|  | } | 
|  | if !exists { | 
|  | return nil, skerr.Fmt("The state file %q doesn't exist.", fileName) | 
|  | } | 
|  | return ret, nil | 
|  | } |