| package main |
| |
| import ( |
| "encoding/json" |
| "errors" |
| "io/ioutil" |
| "os" |
| "strings" |
| |
| "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/shared" |
| "go.skia.org/infra/golden/go/types" |
| ) |
| |
| // imgTestEnv is the environment for the imgtest command ant its sub-commands. |
| type imgTestEnv struct { |
| // Flags used by imgtest:init and imgtest:add. |
| flagCommit string // flag containing the commit hash |
| flagKeysFile string |
| flagIssueID string |
| flagPatchsetID string |
| flagJobID string |
| flagInstanceID string |
| flagWorkDir string |
| flagPassFailStep bool |
| flagFailureFile string |
| flagURL string |
| flagUploadOnly bool |
| |
| // Flags used by imgtest:add |
| flagTestName string |
| flagPNGFile string |
| // a file to a json dictionary of key pairs that will be added to this test |
| // after read into a map[string]string |
| flagTestKeyFile string |
| // a slice of strings like foo:bar that will be split on the first ':' into |
| // key value pairs that will go into a map[string]string |
| flagTestKeys []string |
| } |
| |
| // getImgTestCmd returns the definition of the imgtest command. |
| func getImgTestCmd() *cobra.Command { |
| env := &imgTestEnv{} |
| |
| // imgtest command and its sub commands |
| imgTestCmd := &cobra.Command{ |
| Use: "imgtest", |
| Short: "Collect and upload test results as images", |
| Long: ` |
| Collect and upload test results to the Gold backend.`, |
| } |
| |
| // cmd: imgtest init |
| imgTestInitCmd := &cobra.Command{ |
| Use: "init", |
| Short: "Initialize a testing environment", |
| Long: ` |
| Start a testing session during which tests are added. This initializes the environment. |
| It gathers whether the 'add' command returns a pass/fail value and the common |
| keys shared by all tests that are added via 'add'. |
| `, |
| PreRunE: env.validateFlags, |
| Run: env.runImgTestInitCmd, |
| } |
| env.addCommonFlags(imgTestInitCmd, false) |
| |
| imgTestAddCmd := &cobra.Command{ |
| Use: "add", |
| Short: "Adds a test image to the results.", |
| Long: ` |
| Add images generated by the tests to the test results. This requires two arguments: |
| - The test name |
| - The path to the resulting PNG. |
| `, |
| PreRunE: env.validateFlags, |
| Run: env.runImgTestAddCmd, |
| Args: cobra.NoArgs, |
| } |
| env.addCommonFlags(imgTestAddCmd, true) |
| imgTestAddCmd.Flags().StringVar(&env.flagTestName, "test-name", "", "Unique name of the test, must not contain spaces.") |
| imgTestAddCmd.Flags().StringVar(&env.flagPNGFile, "png-file", "", "Path to the PNG file that contains the test results.") |
| imgTestAddCmd.Flags().StringVar(&env.flagTestKeyFile, "add-test-key-file", "", "A JSON file containing keys and values that should be applied to this test only.") |
| imgTestAddCmd.Flags().StringSliceVar(&env.flagTestKeys, "add-test-key", []string{}, "Any amount of key:value paris that will be added to this test only.") |
| |
| Must(imgTestAddCmd.MarkFlagRequired("test-name")) |
| Must(imgTestAddCmd.MarkFlagRequired("png-file")) |
| |
| imgTestFinalizeCmd := &cobra.Command{ |
| Use: "finalize", |
| Short: "Finish adding tests and process results.", |
| Long: ` |
| All tests have been added. Upload images and generate and upload the JSON file that captures |
| test results.`, |
| Run: env.runImgTestFinalizeCmd, |
| } |
| imgTestFinalizeCmd.Flags().StringVar(&env.flagWorkDir, fstrWorkDir, "", "Work directory for intermediate results") |
| Must(imgTestAddCmd.MarkFlagRequired("work-dir")) |
| |
| imgTestPassFailCmd := &cobra.Command{ |
| Use: "passfail", |
| Short: "Checks whether the results match expectations", |
| Long: ` |
| Check against Gold or local baseline whether the results match the expectations`, |
| Run: env.runImgTestPassFailCmd, |
| } |
| |
| // assemble the imgtest command. |
| imgTestCmd.AddCommand( |
| imgTestInitCmd, |
| imgTestAddCmd, |
| imgTestFinalizeCmd, |
| imgTestPassFailCmd, |
| ) |
| return imgTestCmd |
| } |
| |
| func (i *imgTestEnv) addCommonFlags(cmd *cobra.Command, optional bool) { |
| cmd.Flags().StringVar(&i.flagInstanceID, "instance", "", "ID of the Gold instance.") |
| cmd.Flags().StringVar(&i.flagWorkDir, fstrWorkDir, "", "Work directory for intermediate results") |
| cmd.Flags().BoolVar(&i.flagPassFailStep, "passfail", false, "Whether the 'add' call returns a pass/fail for each test.") |
| cmd.Flags().BoolVar(&i.flagUploadOnly, "upload-only", false, "Skip reading expectations from the server. Incompatible with passfail=true.") |
| |
| cmd.Flags().StringVar(&i.flagCommit, "commit", "", "Git commit hash") |
| cmd.Flags().StringVar(&i.flagKeysFile, "keys-file", "", "JSON file containing key/value pairs commmon to all tests") |
| cmd.Flags().StringVar(&i.flagIssueID, "issue", "", "Gerrit issue if this is trybot run. ") |
| cmd.Flags().StringVar(&i.flagPatchsetID, "patchset", "", "Gerrit patchset number if this is a trybot run. ") |
| cmd.Flags().StringVar(&i.flagJobID, "jobid", "", "Job ID if this is a tryjob run. Current the BuildBucket id.") |
| cmd.Flags().StringVar(&i.flagFailureFile, "failure-file", "", "Path to the file where to write failure information") |
| cmd.Flags().StringVar(&i.flagURL, "url", "", "URL of the Gold instance. Used for testing, if empty the URL will be derived from the value of 'instance'") |
| |
| Must(cmd.MarkFlagRequired(fstrWorkDir)) |
| if !optional { |
| Must(cmd.MarkFlagRequired("instance")) |
| Must(cmd.MarkFlagRequired("commit")) |
| Must(cmd.MarkFlagRequired("keys-file")) |
| } |
| } |
| |
| func (i *imgTestEnv) validateFlags(cmd *cobra.Command, args []string) error { |
| if i.flagUploadOnly && i.flagPassFailStep { |
| return errors.New("Cannot have --upload-only and --passfail both be true.") |
| } |
| if i.flagTestKeyFile != "" && len(i.flagTestKeys) > 0 { |
| return errors.New("Cannot have both --add-test-key and --add-test-key-file.") |
| } |
| return nil |
| } |
| |
| // TODO(kjlubick): Implement this stubbed out command |
| func (i *imgTestEnv) runImgTestPassFailCmd(cmd *cobra.Command, args []string) { notImplemented(cmd) } |
| |
| func (i *imgTestEnv) runImgTestInitCmd(cmd *cobra.Command, args []string) { |
| auth, err := goldclient.LoadAuthOpt(i.flagWorkDir) |
| ifErrLogExit(cmd, err) |
| |
| if auth == nil { |
| logErrf(cmd, "Auth is empty - did you call goldctl auth first?") |
| exitProcess(cmd, 1) |
| } |
| |
| auth.SetDryRun(flagDryRun) |
| |
| keyMap, err := readKeysFile(i.flagKeysFile) |
| ifErrLogExit(cmd, err) |
| |
| validation := shared.Validation{} |
| issueID := validation.Int64Value("issue", i.flagIssueID, types.MasterBranch) |
| patchsetID := validation.Int64Value("patchset", i.flagPatchsetID, 0) |
| jobID := validation.Int64Value("jobid", i.flagJobID, 0) |
| ifErrLogExit(cmd, validation.Errors()) |
| |
| config := goldclient.GoldClientConfig{ |
| FailureFile: i.flagFailureFile, |
| InstanceID: i.flagInstanceID, |
| OverrideGoldURL: i.flagURL, |
| PassFailStep: i.flagPassFailStep, |
| UploadOnly: i.flagUploadOnly, |
| WorkDir: i.flagWorkDir, |
| } |
| goldClient, err := goldclient.NewCloudClient(auth, config) |
| ifErrLogExit(cmd, err) |
| |
| // Define the meta data of the result that is shared by all tests. |
| gr := jsonio.GoldResults{ |
| GitHash: i.flagCommit, |
| Key: keyMap, |
| Issue: issueID, |
| Patchset: patchsetID, |
| BuildBucketID: jobID, |
| } |
| |
| logVerbose(cmd, "Loading hashes and baseline from Gold\n") |
| err = goldClient.SetSharedConfig(gr) |
| ifErrLogExit(cmd, err) |
| |
| logInfof(cmd, "Directory %s successfully loaded with configuration\n", i.flagWorkDir) |
| } |
| |
| // runImgTestCommand processes and uploads test results to Gold. |
| func (i *imgTestEnv) runImgTestAddCmd(cmd *cobra.Command, args []string) { |
| auth, err := goldclient.LoadAuthOpt(i.flagWorkDir) |
| ifErrLogExit(cmd, err) |
| |
| if auth == nil { |
| logErrf(cmd, "Auth is empty - did you call goldctl auth first?") |
| exitProcess(cmd, 1) |
| } |
| |
| auth.SetDryRun(flagDryRun) |
| |
| var goldClient goldclient.GoldClient |
| |
| if i.flagKeysFile != "" { |
| // user has specified a full set of keys. This happens if they |
| // did not (or could not) call init before the start of their test |
| keyMap, err := readKeysFile(i.flagKeysFile) |
| ifErrLogExit(cmd, err) |
| |
| 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, |
| Issue: issueID, |
| Patchset: patchsetID, |
| BuildBucketID: jobID, |
| } |
| |
| config := goldclient.GoldClientConfig{ |
| FailureFile: i.flagFailureFile, |
| InstanceID: i.flagInstanceID, |
| OverrideGoldURL: i.flagURL, |
| PassFailStep: i.flagPassFailStep, |
| UploadOnly: i.flagUploadOnly, |
| WorkDir: i.flagWorkDir, |
| } |
| goldClient, err = goldclient.NewCloudClient(auth, config) |
| ifErrLogExit(cmd, err) |
| |
| err = goldClient.SetSharedConfig(gr) |
| ifErrLogExit(cmd, err) |
| } else { |
| // the user is presumed to have called init first, so we can just load it |
| goldClient, err = goldclient.LoadCloudClient(auth, i.flagWorkDir) |
| ifErrLogExit(cmd, err) |
| } |
| |
| extraKeys := map[string]string{} |
| if i.flagTestKeyFile != "" { |
| j, err := ioutil.ReadFile(i.flagTestKeyFile) |
| if err != nil { |
| logErrf(cmd, "Could not read --add-test-key-file: does it exist? %s", err) |
| exitProcess(cmd, 1) |
| } |
| if err = json.Unmarshal(j, &extraKeys); err != nil { |
| logErrf(cmd, "--add-test-key-file was not a readable JSON object %s", err) |
| exitProcess(cmd, 1) |
| } |
| } else { |
| for _, pair := range i.flagTestKeys { |
| split := strings.SplitN(pair, ":", 2) |
| if len(split) != 2 { |
| logInfof(cmd, "Ignoring malformatted --add-test-key=%s", pair) |
| } else { |
| extraKeys[split[0]] = split[1] |
| } |
| } |
| } |
| |
| pass, err := goldClient.Test(types.TestName(i.flagTestName), i.flagPNGFile, extraKeys) |
| ifErrLogExit(cmd, err) |
| |
| if !pass { |
| logErrf(cmd, "Test: %s FAIL\n", i.flagTestName) |
| exitProcess(cmd, 1) |
| } |
| logInfof(cmd, "Test: %s PASS\n", i.flagTestName) |
| exitProcess(cmd, 0) |
| } |
| |
| func (i *imgTestEnv) runImgTestFinalizeCmd(cmd *cobra.Command, args []string) { |
| auth, err := goldclient.LoadAuthOpt(i.flagWorkDir) |
| ifErrLogExit(cmd, err) |
| |
| if auth == nil { |
| logErrf(cmd, "Auth is empty - did you call goldctl auth first?") |
| exitProcess(cmd, 1) |
| } |
| |
| auth.SetDryRun(flagDryRun) |
| |
| // the user is presumed to have called init and tests first, so we just |
| // have to load it from disk. |
| goldClient, err := goldclient.LoadCloudClient(auth, i.flagWorkDir) |
| ifErrLogExit(cmd, err) |
| |
| logVerbose(cmd, "Uploading the final JSON to Gold\n") |
| err = goldClient.Finalize() |
| ifErrLogExit(cmd, err) |
| exitProcess(cmd, 0) |
| } |
| |
| // readKeysFile is a helper function to read a JSON file with key/value pairs. |
| func readKeysFile(keysFile string) (map[string]string, error) { |
| reader, err := os.Open(keysFile) |
| if err != nil { |
| return nil, err |
| } |
| |
| ret := map[string]string{} |
| err = json.NewDecoder(reader).Decode(&ret) |
| return ret, err |
| } |