| /* |
| Copyright 2018 The Kubernetes Authors. |
| |
| Licensed under the Apache License, Version 2.0 (the "License"); |
| you may not use this file except in compliance with the License. |
| You may obtain a copy of the License at |
| |
| http://www.apache.org/licenses/LICENSE-2.0 |
| |
| Unless required by applicable law or agreed to in writing, software |
| distributed under the License is distributed on an "AS IS" BASIS, |
| WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| See the License for the specific language governing permissions and |
| limitations under the License. |
| */ |
| |
| package workflow |
| |
| import ( |
| "fmt" |
| "reflect" |
| "strings" |
| "testing" |
| |
| "github.com/pkg/errors" |
| "github.com/spf13/cobra" |
| "github.com/spf13/pflag" |
| ) |
| |
| func phaseBuilder(name string, phases ...Phase) Phase { |
| return Phase{ |
| Name: name, |
| Short: fmt.Sprintf("long description for %s ...", name), |
| Phases: phases, |
| } |
| } |
| |
| func TestComputePhaseRunFlags(t *testing.T) { |
| |
| var usecases = []struct { |
| name string |
| options RunnerOptions |
| expected map[string]bool |
| expectedError bool |
| }{ |
| { |
| name: "no options > all phases", |
| options: RunnerOptions{}, |
| expected: map[string]bool{"foo": true, "foo/bar": true, "foo/baz": true, "qux": true}, |
| }, |
| { |
| name: "options can filter phases", |
| options: RunnerOptions{FilterPhases: []string{"foo/baz", "qux"}}, |
| expected: map[string]bool{"foo": false, "foo/bar": false, "foo/baz": true, "qux": true}, |
| }, |
| { |
| name: "options can filter phases - hierarchy is considered", |
| options: RunnerOptions{FilterPhases: []string{"foo"}}, |
| expected: map[string]bool{"foo": true, "foo/bar": true, "foo/baz": true, "qux": false}, |
| }, |
| { |
| name: "options can skip phases", |
| options: RunnerOptions{SkipPhases: []string{"foo/bar", "qux"}}, |
| expected: map[string]bool{"foo": true, "foo/bar": false, "foo/baz": true, "qux": false}, |
| }, |
| { |
| name: "options can skip phases - hierarchy is considered", |
| options: RunnerOptions{SkipPhases: []string{"foo"}}, |
| expected: map[string]bool{"foo": false, "foo/bar": false, "foo/baz": false, "qux": true}, |
| }, |
| { |
| name: "skip options have higher precedence than filter options", |
| options: RunnerOptions{ |
| FilterPhases: []string{"foo"}, // "foo", "foo/bar", "foo/baz" true |
| SkipPhases: []string{"foo/bar"}, // "foo/bar" false |
| }, |
| expected: map[string]bool{"foo": true, "foo/bar": false, "foo/baz": true, "qux": false}, |
| }, |
| { |
| name: "invalid filter option", |
| options: RunnerOptions{FilterPhases: []string{"invalid"}}, |
| expectedError: true, |
| }, |
| { |
| name: "invalid skip option", |
| options: RunnerOptions{SkipPhases: []string{"invalid"}}, |
| expectedError: true, |
| }, |
| } |
| for _, u := range usecases { |
| t.Run(u.name, func(t *testing.T) { |
| var w = Runner{ |
| Phases: []Phase{ |
| phaseBuilder("foo", |
| phaseBuilder("bar"), |
| phaseBuilder("baz"), |
| ), |
| phaseBuilder("qux"), |
| }, |
| } |
| |
| w.prepareForExecution() |
| w.Options = u.options |
| actual, err := w.computePhaseRunFlags() |
| if (err != nil) != u.expectedError { |
| t.Errorf("Unexpected error: %v", err) |
| } |
| if err != nil { |
| return |
| } |
| if !reflect.DeepEqual(actual, u.expected) { |
| t.Errorf("\nactual:\n\t%v\nexpected:\n\t%v\n", actual, u.expected) |
| } |
| }) |
| } |
| } |
| |
| func phaseBuilder1(name string, runIf func(data RunData) (bool, error), phases ...Phase) Phase { |
| return Phase{ |
| Name: name, |
| Short: fmt.Sprintf("long description for %s ...", name), |
| Phases: phases, |
| Run: runBuilder(name), |
| RunIf: runIf, |
| } |
| } |
| |
| var callstack []string |
| |
| func runBuilder(name string) func(data RunData) error { |
| return func(data RunData) error { |
| callstack = append(callstack, name) |
| return nil |
| } |
| } |
| |
| func runConditionTrue(data RunData) (bool, error) { |
| return true, nil |
| } |
| |
| func runConditionFalse(data RunData) (bool, error) { |
| return false, nil |
| } |
| |
| func TestRunOrderAndConditions(t *testing.T) { |
| var w = Runner{ |
| Phases: []Phase{ |
| phaseBuilder1("foo", nil, |
| phaseBuilder1("bar", runConditionTrue), |
| phaseBuilder1("baz", runConditionFalse), |
| ), |
| phaseBuilder1("qux", runConditionTrue), |
| }, |
| } |
| |
| var usecases = []struct { |
| name string |
| options RunnerOptions |
| expectedOrder []string |
| }{ |
| { |
| name: "Run respect runCondition", |
| expectedOrder: []string{"foo", "bar", "qux"}, |
| }, |
| { |
| name: "Run takes options into account", |
| options: RunnerOptions{FilterPhases: []string{"foo"}, SkipPhases: []string{"foo/baz"}}, |
| expectedOrder: []string{"foo", "bar"}, |
| }, |
| } |
| for _, u := range usecases { |
| t.Run(u.name, func(t *testing.T) { |
| callstack = []string{} |
| w.Options = u.options |
| err := w.Run() |
| if err != nil { |
| t.Errorf("Unexpected error: %v", err) |
| } |
| if !reflect.DeepEqual(callstack, u.expectedOrder) { |
| t.Errorf("\ncallstack:\n\t%v\nexpected:\n\t%v\n", callstack, u.expectedOrder) |
| } |
| }) |
| } |
| } |
| |
| func phaseBuilder2(name string, runIf func(data RunData) (bool, error), run func(data RunData) error, phases ...Phase) Phase { |
| return Phase{ |
| Name: name, |
| Short: fmt.Sprintf("long description for %s ...", name), |
| Phases: phases, |
| Run: run, |
| RunIf: runIf, |
| } |
| } |
| |
| func runPass(data RunData) error { |
| return nil |
| } |
| |
| func runFails(data RunData) error { |
| return errors.New("run fails") |
| } |
| |
| func runConditionPass(data RunData) (bool, error) { |
| return true, nil |
| } |
| |
| func runConditionFails(data RunData) (bool, error) { |
| return false, errors.New("run condition fails") |
| } |
| |
| func TestRunHandleErrors(t *testing.T) { |
| var w = Runner{ |
| Phases: []Phase{ |
| phaseBuilder2("foo", runConditionPass, runPass), |
| phaseBuilder2("bar", runConditionPass, runFails), |
| phaseBuilder2("baz", runConditionFails, runPass), |
| }, |
| } |
| |
| var usecases = []struct { |
| name string |
| options RunnerOptions |
| expectedError bool |
| }{ |
| { |
| name: "no errors", |
| options: RunnerOptions{FilterPhases: []string{"foo"}}, |
| }, |
| { |
| name: "run fails", |
| options: RunnerOptions{FilterPhases: []string{"bar"}}, |
| expectedError: true, |
| }, |
| { |
| name: "run condition fails", |
| options: RunnerOptions{FilterPhases: []string{"baz"}}, |
| expectedError: true, |
| }, |
| } |
| for _, u := range usecases { |
| t.Run(u.name, func(t *testing.T) { |
| w.Options = u.options |
| err := w.Run() |
| if (err != nil) != u.expectedError { |
| t.Errorf("Unexpected error: %v", err) |
| } |
| }) |
| } |
| } |
| |
| func phaseBuilder3(name string, hidden bool, phases ...Phase) Phase { |
| return Phase{ |
| Name: name, |
| Short: fmt.Sprintf("long description for %s ...", name), |
| Phases: phases, |
| Hidden: hidden, |
| } |
| } |
| |
| func TestHelp(t *testing.T) { |
| var w = Runner{ |
| Phases: []Phase{ |
| phaseBuilder3("foo", false, |
| phaseBuilder3("bar", false), |
| phaseBuilder3("baz", true), |
| ), |
| phaseBuilder3("qux", false), |
| }, |
| } |
| |
| expected := "The \"myCommand\" command executes the following phases:\n" + |
| "```\n" + |
| "foo long description for foo ...\n" + |
| " /bar long description for bar ...\n" + |
| "qux long description for qux ...\n" + |
| "```" |
| |
| actual := w.Help("myCommand") |
| if !reflect.DeepEqual(actual, expected) { |
| t.Errorf("\nactual:\n\t%v\nexpected:\n\t%v\n", actual, expected) |
| } |
| } |
| |
| func phaseBuilder4(name string, cmdFlags []string, phases ...Phase) Phase { |
| return Phase{ |
| Name: name, |
| Phases: phases, |
| InheritFlags: cmdFlags, |
| } |
| } |
| |
| func phaseBuilder5(name string, flags *pflag.FlagSet) Phase { |
| return Phase{ |
| Name: name, |
| LocalFlags: flags, |
| } |
| } |
| |
| func TestBindToCommand(t *testing.T) { |
| |
| var dummy string |
| localFlags := pflag.NewFlagSet("dummy", pflag.ContinueOnError) |
| localFlags.StringVarP(&dummy, "flag4", "d", "d", "d") |
| |
| var usecases = []struct { |
| name string |
| runner Runner |
| expectedCmdAndFlags map[string][]string |
| setAdditionalFlags func(*pflag.FlagSet) |
| }{ |
| { |
| name: "when there are no phases, cmd should be left untouched", |
| runner: Runner{}, |
| }, |
| { |
| name: "phases should not inherits any parent flags by default", |
| runner: Runner{ |
| Phases: []Phase{phaseBuilder4("foo", nil)}, |
| }, |
| expectedCmdAndFlags: map[string][]string{ |
| "phase foo": {}, |
| }, |
| }, |
| { |
| name: "phases should be allowed to select parent flags to inherits", |
| runner: Runner{ |
| Phases: []Phase{phaseBuilder4("foo", []string{"flag1"})}, |
| }, |
| expectedCmdAndFlags: map[string][]string{ |
| "phase foo": {"flag1"}, //not "flag2" |
| }, |
| }, |
| { |
| name: "it should be possible to apply additional flags to all phases", |
| runner: Runner{ |
| Phases: []Phase{ |
| phaseBuilder4("foo", []string{"flag3"}), |
| phaseBuilder4("bar", []string{"flag1", "flag2", "flag3"}), |
| phaseBuilder4("baz", []string{"flag1"}), //test if additional flags are filtered too |
| }, |
| }, |
| setAdditionalFlags: func(flags *pflag.FlagSet) { |
| var dummy3 string |
| flags.StringVarP(&dummy3, "flag3", "c", "c", "c") |
| }, |
| expectedCmdAndFlags: map[string][]string{ |
| "phase foo": {"flag3"}, |
| "phase bar": {"flag1", "flag2", "flag3"}, |
| "phase baz": {"flag1"}, |
| }, |
| }, |
| { |
| name: "it should be possible to apply custom flags to single phases", |
| runner: Runner{ |
| Phases: []Phase{phaseBuilder5("foo", localFlags)}, |
| }, |
| expectedCmdAndFlags: map[string][]string{ |
| "phase foo": {"flag4"}, |
| }, |
| }, |
| { |
| name: "all the above applies to nested phases too", |
| runner: Runner{ |
| Phases: []Phase{ |
| phaseBuilder4("foo", []string{"flag3"}, |
| phaseBuilder4("bar", []string{"flag1", "flag2", "flag3"}), |
| phaseBuilder4("baz", []string{"flag1"}), //test if additional flags are filtered too |
| phaseBuilder5("qux", localFlags), |
| ), |
| }, |
| }, |
| setAdditionalFlags: func(flags *pflag.FlagSet) { |
| var dummy3 string |
| flags.StringVarP(&dummy3, "flag3", "c", "c", "c") |
| }, |
| expectedCmdAndFlags: map[string][]string{ |
| "phase foo": {"flag3"}, |
| "phase foo bar": {"flag1", "flag2", "flag3"}, |
| "phase foo baz": {"flag1"}, |
| "phase foo qux": {"flag4"}, |
| }, |
| }, |
| } |
| for _, rt := range usecases { |
| t.Run(rt.name, func(t *testing.T) { |
| |
| var dummy1, dummy2 string |
| cmd := &cobra.Command{ |
| Use: "init", |
| } |
| |
| cmd.Flags().StringVarP(&dummy1, "flag1", "a", "a", "a") |
| cmd.Flags().StringVarP(&dummy2, "flag2", "b", "b", "b") |
| |
| if rt.setAdditionalFlags != nil { |
| rt.runner.SetAdditionalFlags(rt.setAdditionalFlags) |
| } |
| |
| rt.runner.BindToCommand(cmd) |
| |
| // in case of no phases, checks that cmd is untouched |
| if len(rt.runner.Phases) == 0 { |
| if cmd.Long != "" { |
| t.Error("cmd.Long is set while it should be leaved untouched\n") |
| } |
| |
| if cmd.Flags().Lookup("skip-phases") != nil { |
| t.Error("cmd has skip-phases flag while it should not\n") |
| } |
| |
| if getCmd(cmd, "phase") != nil { |
| t.Error("cmd has phase subcommand while it should not\n") |
| } |
| |
| return |
| } |
| |
| // Otherwise, if there are phases |
| |
| // Checks that cmd get the description set and the skip-phases flags |
| if cmd.Long == "" { |
| t.Error("cmd.Long not set\n") |
| } |
| |
| if cmd.Flags().Lookup("skip-phases") == nil { |
| t.Error("cmd didn't have skip-phases flag\n") |
| } |
| |
| // Checks that cmd gets a new phase subcommand (without local flags) |
| phaseCmd := getCmd(cmd, "phase") |
| if phaseCmd == nil { |
| t.Error("cmd didn't have phase subcommand\n") |
| return |
| } |
| if err := cmdHasFlags(phaseCmd); err != nil { |
| t.Errorf("command phase didn't have expected flags: %v\n", err) |
| } |
| |
| // Checks that cmd subcommand gets subcommand for phases (without flags properly sets) |
| for c, flags := range rt.expectedCmdAndFlags { |
| |
| cCmd := getCmd(cmd, c) |
| if cCmd == nil { |
| t.Errorf("cmd didn't have %s subcommand\n", c) |
| continue |
| } |
| |
| if err := cmdHasFlags(cCmd, flags...); err != nil { |
| t.Errorf("command %s didn't have expected flags: %v\n", c, err) |
| } |
| } |
| |
| }) |
| } |
| } |
| |
| func getCmd(parent *cobra.Command, nestedName string) *cobra.Command { |
| names := strings.Split(nestedName, " ") |
| for i, n := range names { |
| for _, c := range parent.Commands() { |
| if c.Name() == n { |
| if i == len(names)-1 { |
| return c |
| } |
| parent = c |
| } |
| } |
| } |
| |
| return nil |
| } |
| |
| func cmdHasFlags(cmd *cobra.Command, expectedFlags ...string) error { |
| flags := []string{} |
| cmd.Flags().VisitAll(func(f *pflag.Flag) { |
| flags = append(flags, f.Name) |
| }) |
| |
| for _, e := range expectedFlags { |
| found := false |
| for _, f := range flags { |
| if f == e { |
| found = true |
| } |
| } |
| if !found { |
| return errors.Errorf("flag %q does not exists in %s", e, flags) |
| } |
| } |
| |
| if len(flags) != len(expectedFlags) { |
| return errors.Errorf("expected flags %s, got %s", expectedFlags, flags) |
| } |
| |
| return nil |
| } |