package cmd import ( "context" "fmt" "testing" "time" fn "knative.dev/func/pkg/functions" "knative.dev/func/pkg/mock" ) func TestRun_Run(t *testing.T) { tests := []struct { name string // name of the test desc string // description of the test setup func(fn.Function, *testing.T) error // Optionally mutate function args []string // args for the test case buildError error // Set the builder to yield this error runError error // Set the runner to yield this error buildInvoked bool // should Builder.Build be invoked? runInvoked bool // should Runner.Run be invoked? }{ { name: "run and build by default", desc: "Should run and build when build flag is not specified", args: []string{}, buildInvoked: true, runInvoked: true, }, { name: "run and build flag", desc: "Should run and build when build is merely provided (defaults to true on presence)", args: []string{"--build"}, buildInvoked: true, runInvoked: true, }, { name: "run and build", desc: "Should run and build when build is specifically requested", args: []string{"--build=true"}, buildInvoked: true, runInvoked: true, }, { name: "run and build with builder pack", desc: "Should run and build when build is specifically requested with builder pack", args: []string{"--build=true", "--builder=pack"}, buildInvoked: true, runInvoked: true, }, { name: "run and build with builder s2i", desc: "Should run and build when build is specifically requested with builder s2i", args: []string{"--build=true", "--builder=s2i"}, buildInvoked: true, runInvoked: true, }, { name: "run and build with builder invalid", desc: "Should run and build when build is specifically requested with builder invalid", args: []string{"--build=true", "--builder=invalid"}, buildError: fmt.Errorf("\"invalid\" is not a known builder. Available builders are \"pack\" and \"s2i\""), buildInvoked: true, runInvoked: true, }, { name: "run without build when disabled", desc: "Should run but not build when build is expressly disabled", args: []string{"--build=false"}, // can be any truthy value: 0, 'false' etc. buildInvoked: false, runInvoked: true, }, { name: "run and build on auto", desc: "Should run and buil when build flag set to auto", args: []string{"--build=auto"}, // can be any truthy value: 0, 'false' etc. buildInvoked: true, runInvoked: true, }, { name: "image existence builds", desc: "Should build when image tag exists", // The existence of an image tag value does not mean the function // is built; that is the purvew of the buld stamp staleness check. setup: func(f fn.Function, t *testing.T) error { f.Image = "exampleimage" return f.Write() }, args: []string{}, buildInvoked: true, runInvoked: true, }, { name: "Build errors return", desc: "Errors building cause an immediate return with error", args: []string{}, buildError: fmt.Errorf("generic build error"), buildInvoked: true, runInvoked: false, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { root := fromTempDirectory(t) runner := mock.NewRunner() if tt.runError != nil { runner.RunFn = func(context.Context, fn.Function, time.Duration) (*fn.Job, error) { return nil, tt.runError } } builder := mock.NewBuilder() if tt.buildError != nil { builder.BuildFn = func(f fn.Function) error { return tt.buildError } } // using a command whose client will be populated with mock // builder and mock runner, each of which may be set to error if the // test has an error defined. cmd := NewRunCmd(NewTestClient( fn.WithRunner(runner), fn.WithBuilder(builder), fn.WithRegistry("ghcr.com/reg"), )) cmd.SetArgs(tt.args) // Do not use test command args // set test case's function instance f, err := fn.New().Init(fn.Function{Root: root, Runtime: "go"}) if err != nil { t.Fatal(err) } if tt.setup != nil { if err := tt.setup(f, t); err != nil { t.Fatal(err) } } ctx, cancel := context.WithCancel(context.Background()) runErrCh := make(chan error, 1) go func() { t0 := tt // capture tt into closure _, err := cmd.ExecuteContextC(ctx) if err != nil && t0.buildError != nil { // This is an expected error, so simply continue execution ignoring // the error (send nil on the channel to release the parent routine runErrCh <- nil return } else if err != nil { runErrCh <- err // error not expected return } // No errors, but an error was expected: if t0.buildError != nil { runErrCh <- fmt.Errorf("Expected error: %v but got %v\n", t0.buildError, err) } // Ensure invocations match expectations if builder.BuildInvoked != tt.buildInvoked { runErrCh <- fmt.Errorf("Function was expected to build is: %v but build execution was: %v", tt.buildInvoked, builder.BuildInvoked) } if runner.RunInvoked != tt.runInvoked { runErrCh <- fmt.Errorf("Function was expected to run is: %v but run execution was: %v", tt.runInvoked, runner.RunInvoked) } close(runErrCh) // release the waiting parent process }() cancel() // trigger the return of cmd.ExecuteContextC in the routine <-ctx.Done() if err := <-runErrCh; err != nil { // wait for completion of assertions t.Fatal(err) } }) } }