476 lines
12 KiB
Go
476 lines
12 KiB
Go
/*
|
|
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,
|
|
CmdFlags: cmdFlags,
|
|
}
|
|
}
|
|
|
|
func TestBindToCommand(t *testing.T) {
|
|
|
|
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: "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
|
|
),
|
|
},
|
|
},
|
|
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"},
|
|
},
|
|
},
|
|
}
|
|
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.SetPhaseSubcommandsAdditionalFlags(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
|
|
}
|