[gold] goldctl and backend fixes
- Added goldctl flags for testing
- Synchronized backend and front-end URLs and data structures
- Factored out shared package to hold code used by the frontend
and the backend and to reduce some dependencies not needed by goldctl
(still many dependencies remain)
Bug: skia:8293
Change-Id: I77130c33da38973c680a8e3f1ad867420b0d9c91
Reviewed-on: https://skia-review.googlesource.com/c/178869
Commit-Queue: Stephan Altmueller <stephana@google.com>
Reviewed-by: Ben Wagner <benjaminwagner@google.com>
diff --git a/gold-client/cmd/goldctl/cmd_imgtest.go b/gold-client/cmd/goldctl/cmd_imgtest.go
index 45b6418..132719c 100644
--- a/gold-client/cmd/goldctl/cmd_imgtest.go
+++ b/gold-client/cmd/goldctl/cmd_imgtest.go
@@ -7,7 +7,7 @@
"github.com/spf13/cobra"
"go.skia.org/infra/gold-client/go/goldclient"
"go.skia.org/infra/golden/go/jsonio"
- "go.skia.org/infra/golden/go/search"
+ "go.skia.org/infra/golden/go/shared"
)
// imgTestEnv is the environment for the imgtest command ant its sub-commands.
@@ -22,6 +22,7 @@
flagWorkDir string
flagPassFailStep bool
flagFailureFile string
+ flagURL string
// Flags used by imgtest:add
flagTestName string
@@ -105,6 +106,7 @@
cmd.Flags().StringVarP(&i.flagPatchsetID, "patchset", "", "", "Gerrit patchset number if this is a trybot run. ")
cmd.Flags().StringVarP(&i.flagJobID, "jobid", "", "", "Job ID if this is a tryjob run. Current the BuildBucket id.")
cmd.Flags().StringVarP(&i.flagFailureFile, "failure-file", "", "", "Path to the file where to write failure information")
+ cmd.Flags().StringVarP(&i.flagURL, "url", "", "", "URL of the Gold instance. Used for testing, if empty the URL will be derived from the value of 'instance'")
if !optional {
_ = cmd.MarkFlagRequired("instance")
@@ -125,12 +127,13 @@
keyMap, err := readKeysFile(i.flagKeysFile)
ifErrLogExit(cmd, err)
- validation := search.Validation{}
+ validation := shared.Validation{}
issueID := validation.Int64Value("issue", i.flagIssueID, 0)
patchsetID := validation.Int64Value("patchset", i.flagPatchsetID, 0)
jobID := validation.Int64Value("jobid", i.flagJobID, 0)
ifErrLogExit(cmd, validation.Errors())
+ // Define the meta data of the result that is shared by all tests.
gr := &jsonio.GoldResults{
GitHash: i.flagCommit,
Key: keyMap,
@@ -139,15 +142,23 @@
BuildBucketID: jobID,
}
- goldClient, err := goldclient.NewCloudClient(i.flagWorkDir, i.flagInstanceID, i.flagPassFailStep, gr)
+ config := &goldclient.GoldClientConfig{
+ InstanceID: i.flagInstanceID,
+ WorkDir: i.flagWorkDir,
+ PassFailStep: i.flagPassFailStep,
+ OverrideGoldURL: i.flagURL,
+ }
+ goldClient, err := goldclient.NewCloudClient(config, gr)
ifErrLogExit(cmd, err)
pass, err := goldClient.Test(i.flagTestName, i.flagPNGFile)
ifErrLogExit(cmd, err)
if !pass {
+ logErrf(cmd, "Test: %s FAIL\n", i.flagTestName)
os.Exit(1)
}
+ logInfof(cmd, "Test: %s PASS\n", i.flagTestName)
os.Exit(0)
}
diff --git a/gold-client/go/goldclient/goldclient.go b/gold-client/go/goldclient/goldclient.go
index cd0fc6e..05b2fb3 100644
--- a/gold-client/go/goldclient/goldclient.go
+++ b/gold-client/go/goldclient/goldclient.go
@@ -20,20 +20,35 @@
"go.skia.org/infra/go/httputils"
"go.skia.org/infra/go/skerr"
"go.skia.org/infra/go/util"
+ "go.skia.org/infra/golden/go/baseline"
"go.skia.org/infra/golden/go/diff"
"go.skia.org/infra/golden/go/jsonio"
+ "go.skia.org/infra/golden/go/shared"
"go.skia.org/infra/golden/go/types"
- "go.skia.org/infra/golden/go/web"
)
const (
- resultPrefix = "dm-json-v1"
- imagePrefix = "dm-images-v1"
- knownHashesURLPath = "_/hashes"
+ // resultPrefix is the path prefix in the GCS bucket that holds JSON result files
+ resultPrefix = "dm-json-v1"
- resultStateFile = "result-state.json"
- jsonTempFileName = "dm.json"
- fetchTempFileName = "hashes.txt"
+ // imagePrefix is the path prefix in the GCS bucket that holds images.
+ imagePrefix = "dm-images-v1"
+
+ // knownHashesURLPath is path on the Gold instance to retrieve the known image hashes that do
+ // not need to be uploaded anymore.
+ knownHashesURLPath = "json/hashes"
+
+ // goldURLTmp constructs the URL of the Gold instance from the instance id
+ goldURLTmpl = "https://%s-gold.skia.org"
+
+ // bucketNameTmpl constructs the name of the ingestion bucket from the instance id
+ bucketNameTmpl = "skia-gold-%s"
+
+ // resultStateFile is the name of the file that holds the state in the work directory between calls
+ resultStateFile = "result-state.json"
+
+ // jsonTempFileName is the temporary file that is created to upload results via gsutil.
+ jsonTempFileName = "gsutil_dm.json"
)
// md5Regexp is used to check whether strings are MD5 hashes.
@@ -64,23 +79,34 @@
httpClient *http.Client
}
+// GoldClientConfig is a config structure to configure GoldClient instances
+type GoldClientConfig struct {
+ // WorkDir is a temporary directory that caches data for one run with multiple calls to GoldClient
+ WorkDir string
+
+ // InstanceID is the id of the backend Gold instance
+ InstanceID string
+
+ // PassFailStep indicates whether each call to Test(...) should return a pass/fail value.
+ PassFailStep bool
+
+ // OverrideGoldURL is optional and allows to override the GoldURL for testing.
+ OverrideGoldURL string
+}
+
// NewCloudClient returns an implementation of the GoldClient that relies on the Gold service.
// Arguments:
-// - workDir : is a temporary work directory that needs to be available during the entire
-// testrun (over multiple calls to 'Test')
-// - instanceID: is the id of the Gold instance.
-// - passFailStep: indicates whether each individual call to Test needs to return pass fail.
// - goldResults: A populated instance of jsonio.GoldResults that contains configuration
// shared by all tests.
//
// If a new instance is created for each call to Test, the arguments of the first call are
// preserved. They are cached in a JSON file in the work directory.
-func NewCloudClient(workDir, instanceID string, passFailStep bool, goldResult *jsonio.GoldResults) (GoldClient, error) {
+func NewCloudClient(config *GoldClientConfig, goldResult *jsonio.GoldResults) (GoldClient, error) {
// Make sure the workdir was given and exists.
- if workDir == "" {
+ if config.WorkDir == "" {
return nil, skerr.Fmt("No 'workDir' provided to NewCloudClient")
}
- workDir, err := filepath.Abs(workDir)
+ workDir, err := filepath.Abs(config.WorkDir)
if err != nil {
return nil, err
}
@@ -102,7 +128,7 @@
httpClient: httputils.DefaultClientConfig().Client(),
}
- if err := ret.initResultState(instanceID, passFailStep, goldResult); err != nil {
+ if err := ret.initResultState(config, goldResult); err != nil {
return nil, skerr.Fmt("Error initializing result in cloud GoldClient: %s", err)
}
@@ -165,7 +191,7 @@
// initResultState assembles the information that needs to be uploaded based on previous calls
// to the function and new arguments.
-func (c *cloudClient) initResultState(instanceID string, passFailStep bool, goldResult *jsonio.GoldResults) error {
+func (c *cloudClient) initResultState(config *GoldClientConfig, goldResult *jsonio.GoldResults) error {
// Load the state from the workdir.
var err error
c.resultState, err = loadStateFromJson(c.getResultStateFile())
@@ -180,7 +206,7 @@
// Create a new instance of result state. Setting freshState to true indicates that this needs
// to be stored to disk once a test has been added successfully.
- c.resultState, err = newResultState(goldResult, passFailStep, instanceID, c.workDir, c.httpClient)
+ c.resultState, err = newResultState(goldResult, config, c.workDir, c.httpClient)
if err != nil {
return err
}
@@ -274,18 +300,24 @@
}
// newResultState creates a new instance resultState and downloads the relevant files from Gold.
-func newResultState(goldResult *jsonio.GoldResults, passFailStep bool, instanceID, workDir string, httpClient *http.Client) (*resultState, error) {
+func newResultState(goldResult *jsonio.GoldResults, config *GoldClientConfig, workDir string, httpClient *http.Client) (*resultState, error) {
// TODO(stephana): Move deriving the URLs and the bucket to a central place in the backend
// or get rid of the bucket entirely and expose an upload URL (requires authentication)
+ goldURL := config.OverrideGoldURL
+ if goldURL == "" {
+ goldURL = fmt.Sprintf(goldURLTmpl, config.InstanceID)
+ }
+
ret := &resultState{
GoldResults: goldResult,
- PerTestPassFail: passFailStep,
- InstanceID: instanceID,
- GoldURL: fmt.Sprintf("https://%s-gold.skia.org", instanceID),
- Bucket: fmt.Sprintf("skia-gold-%s", instanceID),
+ PerTestPassFail: config.PassFailStep,
+ InstanceID: config.InstanceID,
+ GoldURL: goldURL,
+ Bucket: fmt.Sprintf(bucketNameTmpl, config.InstanceID),
workDir: workDir,
+ httpClient: httpClient,
}
if err := ret.loadKnownHashes(); err != nil {
@@ -375,16 +407,17 @@
var urlPath string
if r.GoldResults.Issue > 0 {
issueID := strconv.FormatInt(r.GoldResults.Issue, 10)
- urlPath = strings.Replace(web.EXPECATIONS_ISSUE_ROUTE, "{issue_id}", issueID, 1)
+ urlPath = strings.Replace(shared.EXPECATIONS_ISSUE_ROUTE, "{issue_id}", issueID, 1)
} else {
- urlPath = strings.Replace(web.EXPECATIONS_ROUTE, "{commit_hash}", r.GoldResults.GitHash, 1)
+ urlPath = strings.Replace(shared.EXPECATIONS_ROUTE, "{commit_hash}", r.GoldResults.GitHash, 1)
}
- url := fmt.Sprintf("%s/%s", r.GoldURL, urlPath)
+ url := fmt.Sprintf("%s/%s", r.GoldURL, strings.TrimLeft(urlPath, "/"))
resp, err := r.httpClient.Get(url)
if err != nil {
return err
}
+
jsonBytes, err := ioutil.ReadAll(resp.Body)
if err != nil {
return skerr.Fmt("Error reading body of request to %s: %s", url, err)
@@ -393,7 +426,14 @@
return skerr.Fmt("Error closing response from request to %s: %s", url, err)
}
- return json.Unmarshal(jsonBytes, &r.Expectations)
+ exp := &baseline.CommitableBaseLine{}
+
+ if err := json.Unmarshal(jsonBytes, exp); err != nil {
+ return skerr.Fmt("Error parsing JSON: %s", err)
+ }
+
+ r.Expectations = exp.Baseline
+ return nil
}
// getResultFilePath returns that path in GCS where the result file should be stored.
@@ -409,7 +449,7 @@
// Assemble a path that looks like this:
// <path_prefix>/YYYY/MM/DD/HH/<git_hash>/<build_id>/<time_stamp>/<per_run_file_name>.json
- // The first segements up to 'HH' are required so the Gold ingester can scan these prefixes for
+ // 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 times segments of the path are based on UTC location.
diff --git a/golden/cmd/baseline_server/main.go b/golden/cmd/baseline_server/main.go
index 063967e..5f8e8b5 100644
--- a/golden/cmd/baseline_server/main.go
+++ b/golden/cmd/baseline_server/main.go
@@ -15,6 +15,7 @@
"go.skia.org/infra/go/httputils"
"go.skia.org/infra/go/skiaversion"
"go.skia.org/infra/go/sklog"
+ "go.skia.org/infra/golden/go/shared"
"go.skia.org/infra/golden/go/storage"
"go.skia.org/infra/golden/go/web"
gstorage "google.golang.org/api/storage/v1"
@@ -60,8 +61,8 @@
router := mux.NewRouter()
// Retrieving that baseline for master and an Gerrit issue are handled the same way
- router.HandleFunc(web.EXPECATIONS_ROUTE, handlers.JsonBaselineHandler).Methods("GET")
- router.HandleFunc(web.EXPECATIONS_ISSUE_ROUTE, handlers.JsonBaselineHandler).Methods("GET")
+ router.HandleFunc(shared.EXPECATIONS_ROUTE, handlers.JsonBaselineHandler).Methods("GET")
+ router.HandleFunc(shared.EXPECATIONS_ISSUE_ROUTE, handlers.JsonBaselineHandler).Methods("GET")
// Start the server
sklog.Infof("Serving on http://127.0.0.1" + *port)
diff --git a/golden/cmd/skiacorrectness/main.go b/golden/cmd/skiacorrectness/main.go
index 6640e8d..b7217b7 100644
--- a/golden/cmd/skiacorrectness/main.go
+++ b/golden/cmd/skiacorrectness/main.go
@@ -42,6 +42,7 @@
"go.skia.org/infra/golden/go/ignore"
"go.skia.org/infra/golden/go/indexer"
"go.skia.org/infra/golden/go/search"
+ "go.skia.org/infra/golden/go/shared"
"go.skia.org/infra/golden/go/status"
"go.skia.org/infra/golden/go/storage"
"go.skia.org/infra/golden/go/tryjobs"
@@ -424,45 +425,57 @@
router.PathPrefix("/res/").HandlerFunc(web.MakeResourceHandler(*resourcesDir))
router.HandleFunc(OAUTH2_CALLBACK_PATH, login.OAuth2CallbackHandler)
+ // TODO(stephana): remove "/_/hashes" in favor of "/json/hashes" once all clients have switched.
+
// /_/hashes is used by the bots to find hashes it does not need to upload.
router.HandleFunc("/_/hashes", handlers.TextAllHashesHandler).Methods("GET")
router.HandleFunc("/json/version", skiaversion.JsonHandler)
router.HandleFunc("/loginstatus/", login.StatusHandler)
router.HandleFunc("/logout/", login.LogoutHandler)
- // json handlers only used by the new UI.
- router.HandleFunc("/json/byblame", handlers.JsonByBlameHandler).Methods("GET")
- router.HandleFunc("/json/list", handlers.JsonListTestsHandler).Methods("GET")
- router.HandleFunc("/json/paramset", handlers.JsonParamsHandler).Methods("GET")
- router.HandleFunc("/json/commits", handlers.JsonCommitsHandler).Methods("GET")
- router.HandleFunc("/json/diff", handlers.JsonDiffHandler).Methods("GET")
- router.HandleFunc("/json/details", handlers.JsonDetailsHandler).Methods("GET")
- router.HandleFunc("/json/triage", handlers.JsonTriageHandler).Methods("POST")
- router.HandleFunc("/json/clusterdiff", handlers.JsonClusterDiffHandler).Methods("GET")
- router.HandleFunc("/json/cmp", handlers.JsonCompareTestHandler).Methods("POST")
- router.HandleFunc("/json/triagelog", handlers.JsonTriageLogHandler).Methods("GET")
- router.HandleFunc("/json/triagelog/undo", handlers.JsonTriageUndoHandler).Methods("POST")
- router.HandleFunc("/json/failure", handlers.JsonListFailureHandler).Methods("GET")
- router.HandleFunc("/json/failure/clear", handlers.JsonClearFailureHandler).Methods("POST")
- router.HandleFunc("/json/cleardigests", handlers.JsonClearDigests).Methods("POST")
- router.HandleFunc("/json/search", handlers.JsonSearchHandler).Methods("GET")
- router.HandleFunc("/json/export", handlers.JsonExportHandler).Methods("GET")
- router.HandleFunc("/json/tryjob", handlers.JsonTryjobListHandler).Methods("GET")
- router.HandleFunc("/json/tryjob/{id}", handlers.JsonTryjobSummaryHandler).Methods("GET")
+ // Set up a subrouter for the '/json' routes which make up the Gold API.
+ // This makes routing faster, but also returns a failure when an /json route is
+ // requested that doesn't exit.
+ jsonRouter := router.PathPrefix("/json").Subrouter()
+ trim := func(r string) string { return strings.TrimPrefix(r, "/json") }
+
+ jsonRouter.HandleFunc(trim("/json/hashes"), handlers.TextAllHashesHandler).Methods("GET")
+ jsonRouter.HandleFunc(trim("/json/byblame"), handlers.JsonByBlameHandler).Methods("GET")
+ jsonRouter.HandleFunc(trim("/json/list"), handlers.JsonListTestsHandler).Methods("GET")
+ jsonRouter.HandleFunc(trim("/json/paramset"), handlers.JsonParamsHandler).Methods("GET")
+ jsonRouter.HandleFunc(trim("/json/commits"), handlers.JsonCommitsHandler).Methods("GET")
+ jsonRouter.HandleFunc(trim("/json/diff"), handlers.JsonDiffHandler).Methods("GET")
+ jsonRouter.HandleFunc(trim("/json/details"), handlers.JsonDetailsHandler).Methods("GET")
+ jsonRouter.HandleFunc(trim("/json/triage"), handlers.JsonTriageHandler).Methods("POST")
+ jsonRouter.HandleFunc(trim("/json/clusterdiff"), handlers.JsonClusterDiffHandler).Methods("GET")
+ jsonRouter.HandleFunc(trim("/json/cmp"), handlers.JsonCompareTestHandler).Methods("POST")
+ jsonRouter.HandleFunc(trim("/json/triagelog"), handlers.JsonTriageLogHandler).Methods("GET")
+ jsonRouter.HandleFunc(trim("/json/triagelog/undo"), handlers.JsonTriageUndoHandler).Methods("POST")
+ jsonRouter.HandleFunc(trim("/json/failure"), handlers.JsonListFailureHandler).Methods("GET")
+ jsonRouter.HandleFunc(trim("/json/failure/clear"), handlers.JsonClearFailureHandler).Methods("POST")
+ jsonRouter.HandleFunc(trim("/json/cleardigests"), handlers.JsonClearDigests).Methods("POST")
+ jsonRouter.HandleFunc(trim("/json/search"), handlers.JsonSearchHandler).Methods("GET")
+ jsonRouter.HandleFunc(trim("/json/export"), handlers.JsonExportHandler).Methods("GET")
+ jsonRouter.HandleFunc(trim("/json/tryjob"), handlers.JsonTryjobListHandler).Methods("GET")
+ jsonRouter.HandleFunc(trim("/json/tryjob/{id}"), handlers.JsonTryjobSummaryHandler).Methods("GET")
// Retrieving that baseline for master and an Gerrit issue are handled the same way
- router.HandleFunc(web.EXPECATIONS_ROUTE, handlers.JsonBaselineHandler).Methods("GET")
- router.HandleFunc(web.EXPECATIONS_ISSUE_ROUTE, handlers.JsonBaselineHandler).Methods("GET")
- router.HandleFunc("/json/refresh/{id}", handlers.JsonRefreshIssue).Methods("GET")
+ jsonRouter.HandleFunc(trim(shared.EXPECATIONS_ROUTE), handlers.JsonBaselineHandler).Methods("GET")
+ jsonRouter.HandleFunc(trim(shared.EXPECATIONS_ISSUE_ROUTE), handlers.JsonBaselineHandler).Methods("GET")
+ jsonRouter.HandleFunc(trim("/json/refresh/{id}"), handlers.JsonRefreshIssue).Methods("GET")
// Only expose these endpoints if login is enforced across the app or this an open site.
if openSite {
- router.HandleFunc("/json/ignores", handlers.JsonIgnoresHandler).Methods("GET")
- router.HandleFunc("/json/ignores/add/", handlers.JsonIgnoresAddHandler).Methods("POST")
- router.HandleFunc("/json/ignores/del/{id}", handlers.JsonIgnoresDeleteHandler).Methods("POST")
- router.HandleFunc("/json/ignores/save/{id}", handlers.JsonIgnoresUpdateHandler).Methods("POST")
+ jsonRouter.HandleFunc(trim("/json/ignores"), handlers.JsonIgnoresHandler).Methods("GET")
+ jsonRouter.HandleFunc(trim("/json/ignores/add/"), handlers.JsonIgnoresAddHandler).Methods("POST")
+ jsonRouter.HandleFunc(trim("/json/ignores/del/{id}"), handlers.JsonIgnoresDeleteHandler).Methods("POST")
+ jsonRouter.HandleFunc(trim("/json/ignores/save/{id}"), handlers.JsonIgnoresUpdateHandler).Methods("POST")
}
+ // Make sure we return a 404 for anything that starts with /json and could not be found.
+ jsonRouter.HandleFunc("/{ignore:.*}", http.NotFound)
+ router.HandleFunc("/json", http.NotFound)
+
// For everything else serve the same markup.
indexFile := *resourcesDir + "/index.html"
indexTemplate := template.Must(template.New("").ParseFiles(indexFile)).Lookup("index.html")
diff --git a/golden/go/baseline/baseline.go b/golden/go/baseline/baseline.go
index baacb22..e3ac105 100644
--- a/golden/go/baseline/baseline.go
+++ b/golden/go/baseline/baseline.go
@@ -34,8 +34,13 @@
// in tile.
func GetBaselineForMaster(exps types.Expectations, tile *tiling.Tile) *CommitableBaseLine {
commits := tile.Commits
- var startCommit *tiling.Commit = nil
- var endCommit *tiling.Commit = nil
+ if len(tile.Commits) == 0 {
+ return nil
+ }
+
+ // Set the start and end commit in case there are no traces
+ var startCommit *tiling.Commit = tile.Commits[0]
+ var endCommit *tiling.Commit = tile.Commits[len(tile.Commits)-1]
masterBaseline := types.TestExp{}
for _, trace := range tile.Traces {
diff --git a/golden/go/diff/diff.go b/golden/go/diff/diff.go
index 3f63ce6..84bb847 100644
--- a/golden/go/diff/diff.go
+++ b/golden/go/diff/diff.go
@@ -138,7 +138,7 @@
// ImageHandler returns a http.Handler for the given path prefix. The caller
// can then serve images of the format:
// <urlPrefix>/images/<digests>.png
- // <irlPrefix>/diffs/<digest1>-<digests2>.png
+ // <urlPrefix>/diffs/<digest1>-<digests2>.png
ImageHandler(urlPrefix string) (http.Handler, error)
// WarmDigest will fetch the given digests. If sync is true the call will
diff --git a/golden/go/search/parse.go b/golden/go/search/parse.go
index c51d001..f7d0b49 100644
--- a/golden/go/search/parse.go
+++ b/golden/go/search/parse.go
@@ -6,11 +6,10 @@
"io"
"net/http"
"net/url"
- "strconv"
- "strings"
"go.skia.org/infra/go/util"
"go.skia.org/infra/golden/go/diff"
+ "go.skia.org/infra/golden/go/shared"
"go.skia.org/infra/golden/go/types"
)
@@ -85,7 +84,7 @@
}
ctQuery.ColumnQuery.Limit = util.MinInt32(ctQuery.ColumnQuery.Limit, MAX_LIMIT)
- validate := Validation{}
+ validate := shared.Validation{}
// Parse the patchsets.
ctQuery.ColumnQuery.Patchsets = validate.Int64SliceValue("patchsets", ctQuery.ColumnQuery.PatchsetsStr, nil)
@@ -105,123 +104,6 @@
// TODO(stephana): Validation should be factored out into a separate package.
-// Validation is a container to collect error messages during validation of a
-// input with multiple fields.
-type Validation []string
-
-// StrValue validates a string value against containment in a set of options.
-// Argument:
-// name: name of the field being validated.
-// val: value to be validated.
-// options: list of options, one of which value can contain.
-// defaultVal: default value in case val is empty. Can be equal to "".
-// If there is a problem an error message will be added to the Validation object.
-func (v *Validation) StrValue(name string, val *string, options []string, defaultVal string) {
- if *val == "" && defaultVal != "" {
- *val = defaultVal
- return
- }
- if !util.In(*val, options) {
- *v = append(*v, fmt.Sprintf("Field '%s' needs to be one of '%s'", name, strings.Join(options, ",")))
- }
-}
-
-// StrFormValue does the same as StrValue but extracts the given name from
-// the request via r.FormValue(..).
-func (v *Validation) StrFormValue(r *http.Request, name string, val *string, options []string, defaultVal string) {
- *val = r.FormValue(name)
- v.StrValue(name, val, options, defaultVal)
-}
-
-// Float64Value parses the value given in strVal and returns it. If strVal is empty
-// the default value is returned.
-func (v *Validation) Float64Value(name string, strVal string, defaultVal float64) float64 {
- if strVal == "" {
- return defaultVal
- }
-
- tempVal, err := strconv.ParseFloat(strVal, 64)
- if err != nil {
- *v = append(*v, fmt.Sprintf("Field '%s' is not a valid float: %s", name, err))
- }
- return tempVal
-}
-
-// Int64Value parses the value given in strVal and returns it. If strVal is empty
-// the default value is returned.
-func (v *Validation) Int64Value(name string, strVal string, defaultVal int64) int64 {
- if strVal == "" {
- return defaultVal
- }
-
- tempVal, err := strconv.ParseInt(strVal, 10, 64)
- if err != nil {
- *v = append(*v, fmt.Sprintf("Field '%s' is not a valid int: %s", name, err))
- }
- return tempVal
-}
-
-// Float64FormValue does the same as Float64Value but extracts the value from the request object.
-func (v *Validation) Float64FormValue(r *http.Request, name string, defaultVal float64) float64 {
- return v.Float64Value(name, r.FormValue(name), defaultVal)
-}
-
-// Int64FormValue does the same as Int64Value but extracts the value from the request object.
-func (v *Validation) Int64FormValue(r *http.Request, name string, defaultVal int64) int64 {
- return v.Int64Value(name, r.FormValue(name), defaultVal)
-}
-
-// Int64SliceValue parses a comma-separated list of int values and returns them.
-func (v *Validation) Int64SliceValue(name string, strVal string, defaultVal []int64) []int64 {
- if strVal == "" {
- return defaultVal
- }
-
- splitVals := strings.Split(strVal, ",")
- ret := make([]int64, 0, len(splitVals))
- for _, oneStrVal := range splitVals {
- tempVal, err := strconv.ParseInt(oneStrVal, 10, 64)
- if err != nil {
- *v = append(*v, fmt.Sprintf("Field '%s' is not a valid list of comma separated integers: %s", name, err))
- return nil
- }
- ret = append(ret, tempVal)
- }
- return ret
-}
-
-// Int64SliceFormValue does the same as Int64SliceValue but extracts the given
-// name from the request.
-func (v *Validation) Int64SliceFormValue(r *http.Request, name string, defaultVal []int64) []int64 {
- return v.Int64SliceValue(name, r.FormValue(name), defaultVal)
-}
-
-// QueryFormValue extracts a URL-encoded query from the form values and decodes it.
-// If the named field was not available in the given request an empty set url.Values
-// is returned. If an error occurs it will be added to the error list of the validation
-// object.
-func (v *Validation) QueryFormValue(r *http.Request, name string) map[string][]string {
- if q := r.FormValue(name); q != "" {
- ret, err := url.ParseQuery(q)
- if err != nil {
- *v = append(*v, fmt.Sprintf("Unable to parse query: %s. Error: %s", q, err))
- return nil
- }
- return ret
- }
- return map[string][]string{}
-}
-
-// Errors returns a concatenation of all error values accumulated in validation or nil
-// if there were no errors.
-func (v *Validation) Errors() error {
- if len(*v) == 0 {
- return nil
- }
-
- return fmt.Errorf("%s", strings.Join(*v, "\n"))
-}
-
// ParseQuery parses the request parameters from the URL query string or from the
// form parameters and stores the parsed and validated values in query.
func ParseQuery(r *http.Request, query *Query) error {
@@ -240,7 +122,7 @@
query.Match = []string{types.PRIMARY_KEY_FIELD}
}
- validate := Validation{}
+ validate := shared.Validation{}
// Parse the query strings. Note Query and RQuery have different types, but the
// same underlying type: map[string][]string
diff --git a/golden/go/shared/shared.go b/golden/go/shared/shared.go
new file mode 100644
index 0000000..34c05b6
--- /dev/null
+++ b/golden/go/shared/shared.go
@@ -0,0 +1,139 @@
+// Package shared contains constants and functions shared by various backend servers
+// and frontend clients like goldctl.
+package shared
+
+import (
+ "fmt"
+ "net/http"
+ "net/url"
+ "strconv"
+ "strings"
+
+ "go.skia.org/infra/go/util"
+)
+
+// Define common routes used by multiple servers and goldctl
+const (
+ // BASELINE_ROUTE serves the expectations of the master branch
+ EXPECATIONS_ROUTE = "/json/expecations/commit/{commit_hash}"
+
+ // BASELINE_ISSUE_ROUTE serves the baseline for the Gerrit CL identified by 'id'
+ EXPECATIONS_ISSUE_ROUTE = "/json/expecations/issue/{issue_id}"
+)
+
+// Validation is a container to collect error messages during validation of a
+// input with multiple fields.
+type Validation []string
+
+// StrValue validates a string value against containment in a set of options.
+// Argument:
+// name: name of the field being validated.
+// val: value to be validated.
+// options: list of options, one of which value can contain.
+// defaultVal: default value in case val is empty. Can be equal to "".
+// If there is a problem an error message will be added to the Validation object.
+func (v *Validation) StrValue(name string, val *string, options []string, defaultVal string) {
+ if *val == "" && defaultVal != "" {
+ *val = defaultVal
+ return
+ }
+ if !util.In(*val, options) {
+ *v = append(*v, fmt.Sprintf("Field '%s' needs to be one of '%s'", name, strings.Join(options, ",")))
+ }
+}
+
+// StrFormValue does the same as StrValue but extracts the given name from
+// the request via r.FormValue(..).
+func (v *Validation) StrFormValue(r *http.Request, name string, val *string, options []string, defaultVal string) {
+ *val = r.FormValue(name)
+ v.StrValue(name, val, options, defaultVal)
+}
+
+// Float64Value parses the value given in strVal and returns it. If strVal is empty
+// the default value is returned.
+func (v *Validation) Float64Value(name string, strVal string, defaultVal float64) float64 {
+ if strVal == "" {
+ return defaultVal
+ }
+
+ tempVal, err := strconv.ParseFloat(strVal, 64)
+ if err != nil {
+ *v = append(*v, fmt.Sprintf("Field '%s' is not a valid float: %s", name, err))
+ }
+ return tempVal
+}
+
+// Int64Value parses the value given in strVal and returns it. If strVal is empty
+// the default value is returned.
+func (v *Validation) Int64Value(name string, strVal string, defaultVal int64) int64 {
+ if strVal == "" {
+ return defaultVal
+ }
+
+ tempVal, err := strconv.ParseInt(strVal, 10, 64)
+ if err != nil {
+ *v = append(*v, fmt.Sprintf("Field '%s' is not a valid int: %s", name, err))
+ }
+ return tempVal
+}
+
+// Float64FormValue does the same as Float64Value but extracts the value from the request object.
+func (v *Validation) Float64FormValue(r *http.Request, name string, defaultVal float64) float64 {
+ return v.Float64Value(name, r.FormValue(name), defaultVal)
+}
+
+// Int64FormValue does the same as Int64Value but extracts the value from the request object.
+func (v *Validation) Int64FormValue(r *http.Request, name string, defaultVal int64) int64 {
+ return v.Int64Value(name, r.FormValue(name), defaultVal)
+}
+
+// Int64SliceValue parses a comma-separated list of int values and returns them.
+func (v *Validation) Int64SliceValue(name string, strVal string, defaultVal []int64) []int64 {
+ if strVal == "" {
+ return defaultVal
+ }
+
+ splitVals := strings.Split(strVal, ",")
+ ret := make([]int64, 0, len(splitVals))
+ for _, oneStrVal := range splitVals {
+ tempVal, err := strconv.ParseInt(oneStrVal, 10, 64)
+ if err != nil {
+ *v = append(*v, fmt.Sprintf("Field '%s' is not a valid list of comma separated integers: %s", name, err))
+ return nil
+ }
+ ret = append(ret, tempVal)
+ }
+ return ret
+}
+
+// Int64SliceFormValue does the same as Int64SliceValue but extracts the given
+// name from the request.
+func (v *Validation) Int64SliceFormValue(r *http.Request, name string, defaultVal []int64) []int64 {
+ return v.Int64SliceValue(name, r.FormValue(name), defaultVal)
+}
+
+// QueryFormValue extracts a URL-encoded query from the form values and decodes it.
+// If the named field was not available in the given request an empty set url.Values
+// is returned. If an error occurs it will be added to the error list of the validation
+// object.
+func (v *Validation) QueryFormValue(r *http.Request, name string) map[string][]string {
+ if q := r.FormValue(name); q != "" {
+ ret, err := url.ParseQuery(q)
+ if err != nil {
+ *v = append(*v, fmt.Sprintf("Unable to parse query: %s. Error: %s", q, err))
+ return nil
+ }
+ return ret
+ }
+ return map[string][]string{}
+}
+
+// Errors returns a concatenation of all error values accumulated in validation or nil
+// if there were no errors.
+func (v *Validation) Errors() error {
+ if len(*v) == 0 {
+ return nil
+ }
+
+ return fmt.Errorf("%s", strings.Join(*v, "\n"))
+}
diff --git a/golden/go/storage/baseliner.go b/golden/go/storage/baseliner.go
index e34e77e..72bbd54 100644
--- a/golden/go/storage/baseliner.go
+++ b/golden/go/storage/baseliner.go
@@ -1,6 +1,8 @@
package storage
import (
+ "github.com/davecgh/go-spew/spew"
+ "go.skia.org/infra/go/skerr"
"go.skia.org/infra/go/sklog"
"go.skia.org/infra/go/tiling"
"go.skia.org/infra/golden/go/baseline"
@@ -44,18 +46,23 @@
// PushMasterBaselines writes the baselines for the master branch to GCS.
func (b *Baseliner) PushMasterBaselines(tile *tiling.Tile) error {
if !b.CanWriteBaseline() {
- return sklog.FmtErrorf("Trying to write baseline while GCS path is not configured.")
+ return skerr.Fmt("Trying to write baseline while GCS path is not configured.")
}
_, baseLine, err := b.getMasterBaseline(tile)
if err != nil {
- return sklog.FmtErrorf("Error retrieving master baseline: %s", err)
+ return skerr.Fmt("Error retrieving master baseline: %s", err)
+ }
+
+ if baseLine == nil {
+ sklog.Infof("No baseline available.")
+ return nil
}
// Write the baseline to GCS.
outputPath, err := b.gStorageClient.WriteBaseLine(baseLine)
if err != nil {
- return sklog.FmtErrorf("Error writing baseline to GCS: %s", err)
+ return skerr.Fmt("Error writing baseline to GCS: %s", err)
}
sklog.Infof("Baseline for master written to %s.", outputPath)
return nil
@@ -64,18 +71,18 @@
// PushIssueBaseline writes the baseline for a Gerrit issue to GCS.
func (b *Baseliner) PushIssueBaseline(issueID int64, tile *tiling.Tile, tallies *tally.Tallies) error {
if !b.CanWriteBaseline() {
- return sklog.FmtErrorf("Trying to write baseline while GCS path is not configured.")
+ return skerr.Fmt("Trying to write baseline while GCS path is not configured.")
}
issueExpStore := b.issueExpStoreFactory(issueID)
exp, err := issueExpStore.Get()
if err != nil {
- return sklog.FmtErrorf("Unable to get issue expecations: %s", err)
+ return skerr.Fmt("Unable to get issue expecations: %s", err)
}
tryjobs, tryjobResults, err := b.tryjobStore.GetTryjobs(issueID, nil, true, true)
if err != nil {
- return sklog.FmtErrorf("Unable to get TryjobResults")
+ return skerr.Fmt("Unable to get TryjobResults")
}
talliesByTest := tallies.ByTest()
baseLine := baseline.GetBaselineForIssue(issueID, tryjobs, tryjobResults, exp, tile.Commits, talliesByTest)
@@ -83,7 +90,7 @@
// Write the baseline to GCS.
outputPath, err := b.gStorageClient.WriteBaseLine(baseLine)
if err != nil {
- return sklog.FmtErrorf("Error writing baseline to GCS: %s", err)
+ return skerr.Fmt("Error writing baseline to GCS: %s", err)
}
sklog.Infof("Baseline for issue %d written to %s.", issueID, outputPath)
return nil
@@ -100,6 +107,7 @@
egroup.Go(func() error {
var err error
masterBaseline, err = b.gStorageClient.ReadBaseline(commitHash, 0)
+ sklog.Infof("Master: %s %s", commitHash, spew.Sdump(masterBaseline))
return err
})
@@ -107,6 +115,7 @@
egroup.Go(func() error {
var err error
issueBaseline, err = b.gStorageClient.ReadBaseline(commitHash, issueID)
+ sklog.Infof("issue %s %d: %s", commitHash, issueID, spew.Sdump(issueBaseline))
return err
})
}
@@ -125,7 +134,7 @@
func (b *Baseliner) getMasterBaseline(tile *tiling.Tile) (types.Expectations, *baseline.CommitableBaseLine, error) {
exps, err := b.expectationsStore.Get()
if err != nil {
- return nil, nil, sklog.FmtErrorf("Unable to retrieve expectations: %s", err)
+ return nil, nil, skerr.Fmt("Unable to retrieve expectations: %s", err)
}
return exps, baseline.GetBaselineForMaster(exps, tile), nil
diff --git a/golden/go/storage/gsclient.go b/golden/go/storage/gsclient.go
index 7b25493..52ca50b 100644
--- a/golden/go/storage/gsclient.go
+++ b/golden/go/storage/gsclient.go
@@ -96,7 +96,10 @@
if err != nil {
// If the item doesn't exist we return an empty baseline
if err == gstorage.ErrObjectNotExist {
- return &baseline.CommitableBaseLine{Baseline: types.TestExp{}}, nil
+ return &baseline.CommitableBaseLine{
+ Baseline: types.TestExp{},
+ Issue: issueID,
+ }, nil
}
return nil, sklog.FmtErrorf("Error fetching attributes of baseline file: %s", err)
}
diff --git a/golden/go/storage/storage_test.go b/golden/go/storage/storage_test.go
index 35e2b2a..dbe1559 100644
--- a/golden/go/storage/storage_test.go
+++ b/golden/go/storage/storage_test.go
@@ -139,8 +139,8 @@
}()
// Read the master baseline that has not been written
- emptyBaseline := &baseline.CommitableBaseLine{Baseline: types.TestExp{}}
- foundBaseline, err := gsClient.ReadBaseline("", 0)
+ emptyBaseline := &baseline.CommitableBaseLine{Baseline: types.TestExp{}, Issue: 5344}
+ foundBaseline, err := gsClient.ReadBaseline("", 5344)
assert.NoError(t, err)
assert.Equal(t, emptyBaseline, foundBaseline)
diff --git a/golden/go/web/web.go b/golden/go/web/web.go
index 62d3599..9280506 100644
--- a/golden/go/web/web.go
+++ b/golden/go/web/web.go
@@ -26,6 +26,7 @@
"go.skia.org/infra/golden/go/ignore"
"go.skia.org/infra/golden/go/indexer"
"go.skia.org/infra/golden/go/search"
+ "go.skia.org/infra/golden/go/shared"
"go.skia.org/infra/golden/go/status"
"go.skia.org/infra/golden/go/storage"
"go.skia.org/infra/golden/go/summary"
@@ -34,15 +35,6 @@
"go.skia.org/infra/golden/go/validation"
)
-// Define common routes used by multiple servers
-const (
- // BASELINE_ROUTE serves the expectations of the master branch
- EXPECATIONS_ROUTE = "/json/expecations/commit/{commit_hash}"
-
- // BASELINE_ISSUE_ROUTE serves the baseline for the Gerrit CL identified by 'id'
- EXPECATIONS_ISSUE_ROUTE = "/json/expecations/issue/{issue_id}"
-)
-
const (
// DEFAULT_PAGE_SIZE is the default page size used for pagination.
DEFAULT_PAGE_SIZE = 20
@@ -871,7 +863,7 @@
q := r.URL.Query()
offset, size, err := httputils.PaginationParams(q, 0, DEFAULT_PAGE_SIZE, MAX_PAGE_SIZE)
if err == nil {
- validate := search.Validation{}
+ validate := shared.Validation{}
issue := validate.Int64Value("issue", q.Get("issue"), 0)
if err := validate.Errors(); err != nil {
httputils.ReportError(w, r, err, "Unable to retrieve triage log.")
@@ -1027,6 +1019,15 @@
httputils.ReportError(w, r, err, "Issue ID must be valid integer.")
return
}
+
+ // Retrieve the commit at HEAD
+ tmpHashes := wh.Storages.Git.LastN(r.Context(), 1)
+ if len(tmpHashes) == 0 {
+ msg := "No commit information available"
+ httputils.ReportError(w, r, skerr.Fmt(msg), msg)
+ return
+ }
+ commitHash = tmpHashes[0]
} else {
// Since this was not called for an issue, we need to extract a Git hash.
var ok bool