Repository: uber-go/fx
Pull request #706 (2020-05-29 21:55:45)

Introduce fx.ValidateGraph which validates graph dependencies without invoking them
`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 https://github.com/uber-go/dig/pull/266

Ref T3706201

codecov[bot]: # [Codecov](https://codecov.io/gh/uber-go/fx/pull/706?src=pr&el=h1) Report
> Merging [#706](https://codecov.io/gh/uber-go/fx/pull/706?src=pr&el=desc) into [master](https://codecov.io/gh/uber-go/fx/commit/86cb584cbc7e7b599e9768c4393eda768e8b3d72&el=desc) will **decrease** coverage by `0.45%`.
> The diff coverage is `82.35%`.

[![Impacted file tree graph](https://codecov.io/gh/uber-go/fx/pull/706/graphs/tree.svg?width=650&height=150&src=pr&token=PYY9PSfaC6)](https://codecov.io/gh/uber-go/fx/pull/706?src=pr&el=tree)

```diff
@@            Coverage Diff             @@
##           master     #706      +/-   ##
==========================================
- Coverage   98.26%   97.80%   -0.46%     
==========================================
  Files          16       16              
  Lines         576      592      +16     
==========================================
+ Hits          566      579      +13     
- Misses          8       11       +3     
  Partials        2        2              
```


| [Impacted Files](https://codecov.io/gh/uber-go/fx/pull/706?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [app.go](https://codecov.io/gh/uber-go/fx/pull/706/diff?src=pr&el=tree#diff-YXBwLmdv) | `96.32% <82.35%> (-1.06%)` | :arrow_down: |

------

[Continue to review full report at Codecov](https://codecov.io/gh/uber-go/fx/pull/706?src=pr&el=continue).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/uber-go/fx/pull/706?src=pr&el=footer). Last update [86cb584...30304ec](https://codecov.io/gh/uber-go/fx/pull/706?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 38d0cb6..b29a63b 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -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
diff --git a/app.go b/app.go
index 423f8f4..2095d87 100644
--- a/app.go
+++ b/app.go
@@ -339,6 +339,7 @@ type App struct {
 	startTimeout time.Duration
 	stopTimeout  time.Duration
 	errorHooks   []ErrorHandler
+	validate     bool
 
 	donesMu sync.RWMutex
 	dones   []chan os.Signal
@@ -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.
@@ -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,
@@ -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)
 	}
diff --git a/go.mod b/go.mod
index 1eb289d..cc264e7 100644
--- a/go.mod
+++ b/go.mod
@@ -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
diff --git a/go.sum b/go.sum
index f304185..18347db 100644
--- a/go.sum
+++ b/go.sum
@@ -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=


Test patch:
diff --git a/app_internal_test.go b/app_internal_test.go
index 805d84c..55e0836 100644
--- a/app_internal_test.go
+++ b/app_internal_test.go
@@ -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) {
@@ -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())
+}
diff --git a/app_test.go b/app_test.go
index be68d87..6423bee 100644
--- a/app_test.go
+++ b/app_test.go
@@ -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.")