Skip to content

Commit

Permalink
Introduce fx.ValidateGraph which validates graph dependencies without…
Browse files Browse the repository at this point in the history
… invoking them (uber-go#706)

fx.ValidateGraph allows graph validation for correct dependencies and lack of cycles without running anything. This is useful if an Invoke has side-effects, does IO, etc.

Depends on uber-go/dig#266

Ref T3706201
  • Loading branch information
shirchen authored Jun 16, 2020
1 parent 86cb584 commit 6b37a98
Show file tree
Hide file tree
Showing 6 changed files with 123 additions and 5 deletions.
4 changes: 3 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).

## [Unreleased]
- No changes yet.
### Added
- Added `fx.ValidateGraph` which allows graph cycle validation and dependency correctness
without running anything. This is useful if `fx.Invoke` has side effects, does I/O, etc.

## [1.12.0] - 2020-04-09
### Added
Expand Down
35 changes: 34 additions & 1 deletion app.go
Original file line number Diff line number Diff line change
Expand Up @@ -339,6 +339,7 @@ type App struct {
startTimeout time.Duration
stopTimeout time.Duration
errorHooks []ErrorHandler
validate bool

donesMu sync.RWMutex
dones []chan os.Signal
Expand Down Expand Up @@ -399,6 +400,34 @@ func (ehl errorHandlerList) HandleError(err error) {
}
}

// validate sets *App into validation mode without running invoked functions.
func validate(validate bool) Option {
return &validateOption{
validate: validate,
}
}

type validateOption struct {
validate bool
}

func (o validateOption) apply(app *App) {
app.validate = o.validate
}

func (o validateOption) String() string {
return fmt.Sprintf("fx.validate(%v)", o.validate)
}

// ValidateApp validates that supplied graph would run and is not missing any dependencies. This
// method does not invoke actual input functions.
func ValidateApp(opts ...Option) error {
opts = append(opts, validate(true))
app := New(opts...)

return app.Err()
}

// New creates and initializes an App, immediately executing any functions
// registered via Invoke options. See the documentation of the App struct for
// details on the application's initialization, startup, and shutdown logic.
Expand All @@ -407,7 +436,6 @@ func New(opts ...Option) *App {
lc := &lifecycleWrapper{lifecycle.New(logger)}

app := &App{
container: dig.New(dig.DeferAcyclicVerification()),
lifecycle: lc,
logger: logger,
startTimeout: DefaultTimeout,
Expand All @@ -418,6 +446,11 @@ func New(opts ...Option) *App {
opt.apply(app)
}

app.container = dig.New(
dig.DeferAcyclicVerification(),
dig.DryRun(app.validate),
)

for _, p := range app.provides {
app.provide(p)
}
Expand Down
11 changes: 11 additions & 0 deletions app_internal_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,14 @@
package fx

import (
"fmt"
"os"
"sync"
"syscall"
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func TestAppRun(t *testing.T) {
Expand All @@ -41,3 +45,10 @@ func TestAppRun(t *testing.T) {
done <- syscall.SIGINT
wg.Wait()
}

// TestValidateString verifies private option. Public options are tested in app_test.go.
func TestValidateString(t *testing.T) {
stringer, ok := validate(true).(fmt.Stringer)
require.True(t, ok, "option must implement stringer")
assert.Equal(t, "fx.validate(true)", stringer.String())
}
72 changes: 72 additions & 0 deletions app_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -681,6 +681,78 @@ func TestAppStop(t *testing.T) {
})
}

func TestValidateApp(t *testing.T) {
t.Run("do not run provides on graph validation", func(t *testing.T) {
type type1 struct{}
err := ValidateApp(
Provide(func() *type1 {
t.Error("provide must not be called")
return nil
}),
Invoke(func(*type1) {}),
)
require.NoError(t, err)
})
t.Run("do not run provides nor invokes on graph validation", func(t *testing.T) {
type type1 struct{}
err := ValidateApp(
Provide(func() *type1 {
t.Error("provide must not be called")
return nil
}),
Invoke(func(*type1) {
t.Error("invoke must not be called")
}),
)
require.NoError(t, err)
})
t.Run("provide depends on something not available", func(t *testing.T) {
type type1 struct{}
err := ValidateApp(
Provide(func(type1) int { return 0 }),
Invoke(func(int) error { return nil }),
)
require.Error(t, err, "fx.ValidateApp should error on argument not available")
errMsg := err.Error()
assert.Contains(t, errMsg, "could not build arguments for function")
assert.Contains(t, errMsg, "failed to build int: missing dependencies for function")
assert.Contains(t, errMsg, "missing type: fx_test.type1")
})
t.Run("provide introduces a cycle", func(t *testing.T) {
type A struct{}
type B struct{}
err := ValidateApp(
Provide(func(A) B { return B{} }),
Provide(func(B) A { return A{} }),
Invoke(func(B) {}),
)
require.Error(t, err, "fx.ValidateApp should error on cycle")
errMsg := err.Error()
assert.Contains(t, errMsg, "cycle detected in dependency graph")
})
t.Run("invoke a type that's not available", func(t *testing.T) {
type A struct{}
err := ValidateApp(
Invoke(func(A) {}),
)
require.Error(t, err, "fx.ValidateApp should return an error on missing invoke dep")
errMsg := err.Error()
assert.Contains(t, errMsg, "missing dependencies for function")
assert.Contains(t, errMsg, "missing type: fx_test.A")
})
t.Run("no error", func(t *testing.T) {
type A struct{}
err := ValidateApp(
Provide(func() A {
return A{}
}),
Invoke(func(A) {}),
)
require.NoError(t, err, "fx.ValidateApp should not return an error")
})

}

func TestDone(t *testing.T) {
done := fxtest.New(t).Done()
require.NotNil(t, done, "Got a nil channel.")
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ go 1.13

require (
github.com/stretchr/testify v1.4.0
go.uber.org/dig v1.9.0
go.uber.org/dig v1.10.0
go.uber.org/goleak v0.10.0
go.uber.org/multierr v1.4.0
golang.org/x/lint v0.0.0-20190930215403-16217165b5de
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,8 @@ github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJy
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
go.uber.org/atomic v1.5.0 h1:OI5t8sDa1Or+q8AeE+yKeB/SDYioSHAgcVljj9JIETY=
go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
go.uber.org/dig v1.9.0 h1:pJTDXKEhRqBI8W7rU7kwT5EgyRZuSMVSFcZolOvKK9U=
go.uber.org/dig v1.9.0/go.mod h1:X34SnWGr8Fyla9zQNO2GSO2D+TIuqB14OS8JhYocIyw=
go.uber.org/dig v1.10.0 h1:yLmDDj9/zuDjv3gz8GQGviXMs9TfysIUMUilCpgzUJY=
go.uber.org/dig v1.10.0/go.mod h1:X34SnWGr8Fyla9zQNO2GSO2D+TIuqB14OS8JhYocIyw=
go.uber.org/goleak v0.10.0 h1:G3eWbSNIskeRqtsN/1uI5B+eP73y3JUuBsv9AZjehb4=
go.uber.org/goleak v0.10.0/go.mod h1:VCZuO8V8mFPlL0F5J5GK1rtHV3DrFcQ1R8ryq7FK0aI=
go.uber.org/multierr v1.4.0 h1:f3WCSC2KzAcBXGATIxAB1E2XuCpNU255wNKZ505qi3E=
Expand Down

0 comments on commit 6b37a98

Please sign in to comment.