blob: e62a04084330caa0e4da718c279ca661a791dfa2 [file] [log] [blame]
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
}