~marcopolo/di

1b53f1a8ea5539f5180fe7f1034079b92861fdeb — Marco Munizaga 9 months ago db40d19
Refactor tests
1 files changed, 344 insertions(+), 305 deletions(-)

M di_test.go
M di_test.go => di_test.go +344 -305
@@ 74,346 74,385 @@ func TestBuildSuccess(t *testing.T) {
		a  *A
		cs []C
	}

	type Config struct {
		MakeA  Provide[*A]
		MakeB  Provide[*B]
		MakeCs []Provide[C]
	}

	cfg := Config{
		MakeA: MustProvide[*A](func() (*A, error) {
			return &A{val: "hello"}, nil
		}),
		MakeB: MustProvide[*B](func(a *A, cs []C) *B {
			return &B{a: a, cs: cs}
		}),
		MakeCs: []Provide[C]{
			MustProvide[C](C{val: 1}),
			MustProvide[C](func() (C, error) {
				return C{val: 2}, nil
			})},
	}

	type Result struct {
		A *A
		B *B
	}
	var res Result
	err := Build(cfg, &res)
	if err != nil {
		t.Fatalf("Build failed: %v", err)
	}
	if res.A == nil {
		t.Fatalf("expected res.A to be populated")
	}
	if res.B == nil {
		t.Fatalf("expected res.B to be populated")
	}
	if res.B.a != res.A {
		t.Fatalf("expected B.a to reference A instance")
	}
	if len(res.B.cs) != 2 {
		t.Fatalf("wrong count. Saw %d", len(res.B.cs))
	}
	if res.B.cs[0].val != 1 {
		t.Fatalf("wrong value")
	}
	if res.B.cs[1].val != 2 {
		t.Fatalf("wrong value")
	}
	if res.A.val != "hello" {
		t.Fatalf("unexpected A value: %s", res.A.val)
	}
}

func TestBuildSuccess2(t *testing.T) {
	type A struct {
		val string
	}
	type B struct {
		a *A
	}

	type Config struct {
		MakeA func() (*A, error)
		MakeB func(*A) (*B, error)
	}

	type Result struct {
		A *A
	type NestedConfig struct {
		OtherSetting   bool
		NestedDecision func(c NestedConfig) uint
	}

	cfg := Config{
		MakeA: func() (*A, error) {
			return &A{val: "hello"}, nil
	type ANum int
	type ConfigWithInner struct {
		NestedConfig
		SomeSetting bool
		Inner       func(c ConfigWithInner) int
	}

	tests := []struct {
		name   string
		config interface{}
		result interface{}
		verify func(t *testing.T, result interface{})
	}{
		{
			name: "complex dependencies with providers",
			config: struct {
				MakeA  Provide[*A]
				MakeB  Provide[*B]
				MakeCs []Provide[C]
			}{
				MakeA: MustProvide[*A](func() (*A, error) {
					return &A{val: "hello"}, nil
				}),
				MakeB: MustProvide[*B](func(a *A, cs []C) *B {
					return &B{a: a, cs: cs}
				}),
				MakeCs: []Provide[C]{
					MustProvide[C](C{val: 1}),
					MustProvide[C](func() (C, error) {
						return C{val: 2}, nil
					}),
				},
			},
			result: &struct {
				A *A
				B *B
			}{},
			verify: func(t *testing.T, result interface{}) {
				res := result.(*struct {
					A *A
					B *B
				})
				if res.A == nil {
					t.Fatalf("expected res.A to be populated")
				}
				if res.B == nil {
					t.Fatalf("expected res.B to be populated")
				}
				if res.B.a != res.A {
					t.Fatalf("expected B.a to reference A instance")
				}
				if len(res.B.cs) != 2 {
					t.Fatalf("wrong count. Saw %d", len(res.B.cs))
				}
				if res.B.cs[0].val != 1 {
					t.Fatalf("wrong value")
				}
				if res.B.cs[1].val != 2 {
					t.Fatalf("wrong value")
				}
				if res.A.val != "hello" {
					t.Fatalf("unexpected A value: %s", res.A.val)
				}
			},
		},
		MakeB: func(a *A) (*B, error) {
			panic("Unexpected call to MakeB")
			// (removed unreachable code after panic)
		{
			name: "simple function constructors",
			config: struct {
				MakeA func() (*A, error)
				MakeB func(*A) (*B, error)
			}{
				MakeA: func() (*A, error) {
					return &A{val: "hello"}, nil
				},
				MakeB: func(a *A) (*B, error) {
					panic("Unexpected call to MakeB")
				},
			},
			result: &struct {
				A *A
			}{},
			verify: func(t *testing.T, result interface{}) {
				res := result.(*struct {
					A *A
				})
				if res.A == nil {
					t.Fatalf("expected res.A to be populated")
				}
				if res.A.val != "hello" {
					t.Fatalf("unexpected A value: %s", res.A.val)
				}
			},
		},
	}

	var res Result
	err := Build(cfg, &res)
	if err != nil {
		t.Fatalf("Build failed: %v", err)
	}
	if res.A == nil {
		t.Fatalf("expected res.A to be populated")
	}
	if res.A.val != "hello" {
		t.Fatalf("unexpected A value: %s", res.A.val)
	}
}

// Test that constructor error is propagated.
func TestBuildConstructorError(t *testing.T) {
	type A struct{}
	sentinel := errors.New("boom")

	type Config struct {
		MakeA func() (*A, error)
	}
	type Result struct {
		A *A
	}

	cfg := Config{
		MakeA: func() (*A, error) {
			return nil, sentinel
		{
			name: "pre-supplied values",
			config: struct {
				A  *A
				MB func(*A) (*B, error)
			}{
				A: &A{val: "pre-supplied"},
				MB: func(a *A) (*B, error) {
					return &B{a: a}, nil
				},
			},
			result: &struct {
				A *A
				B *B
			}{},
			verify: func(t *testing.T, result interface{}) {
				res := result.(*struct {
					A *A
					B *B
				})
				if res.A == nil || res.A.val != "pre-supplied" {
					t.Fatalf("expected pre-supplied A, got %+v", res.A)
				}
				if res.B == nil || res.B.a != res.A {
					t.Fatalf("expected B referencing A, got %+v", res.B)
				}
			},
		},
		{
			name: "type aliases",
			config: struct {
				A ANum
				B int
			}{A: 3, B: 4},
			result: &struct {
				A ANum
			}{},
			verify: func(t *testing.T, result interface{}) {
				res := result.(*struct {
					A ANum
				})
				if res.A != 3 {
					t.Fatalf("expected A=3, got %v", res.A)
				}
			},
		},
		{
			name: "reference config in constructors",
			config: ConfigWithInner{
				SomeSetting: true,
				Inner: func(c ConfigWithInner) int {
					if c.SomeSetting {
						return 1
					}
					return 0
				},
				NestedConfig: NestedConfig{
					OtherSetting: true,
					NestedDecision: func(c NestedConfig) uint {
						if c.OtherSetting {
							return 1
						}
						return 0
					},
				},
			},
			result: &struct {
				A int
				B uint
			}{},
			verify: func(t *testing.T, result interface{}) {
				res := result.(*struct {
					A int
					B uint
				})
				if res.A != 1 {
					t.Fatalf("expected A=1, got %v", res.A)
				}
				if res.B != 1 {
					t.Fatalf("expected B=1, got %v", res.B)
				}
			},
		},
	}
	var res Result
	err := Build(cfg, &res)
	if err == nil {
		t.Fatalf("expected error")
	}
	if !strings.Contains(err.Error(), "MakeA") {
		t.Fatalf("expected error to mention constructor name, got: %v", err)
	}
	if !strings.Contains(err.Error(), "boom") {
		t.Fatalf("expected original error message, got: %v", err)
	}
	if res.A != nil {
		t.Fatalf("result A should not be populated on constructor failure")

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			err := Build(tt.config, tt.result)
			if err != nil {
				t.Fatalf("Build failed: %v", err)
			}
			tt.verify(t, tt.result)
		})
	}
}

// Test missing dependency (constructor requires *A but *A not provided).
func TestBuildMissingDependency(t *testing.T) {
func TestBuildErrors(t *testing.T) {
	type A struct{}
	type B struct {
		a *A
	}

	type Config struct {
		MakeB func(*A) (*B, error)
	}
	type Result struct {
		B *B
	}

	cfg := Config{
		MakeB: func(a *A) (*B, error) {
			return &B{a: a}, nil
		},
	}
	var res Result
	err := Build(cfg, &res)
	if err == nil {
		t.Fatalf("expected missing dependency error")
	}
	// Parameter type string should appear (may be *di.A).
	if !strings.Contains(err.Error(), "*di.A") && !strings.Contains(err.Error(), "di.A") {
		t.Fatalf("expected error to mention missing type *di.A, got: %v", err)
	}
	if res.B != nil {
		t.Fatalf("result B should not be populated")
	}
}

// Test cycle detection between X and Y.
func TestBuildCycleDetection(t *testing.T) {
	type X struct{}
	type Y struct{}

	type Config struct {
		MakeX func(*Y) *X
		MakeY func(*X) *Y
	}
	type Result struct {
		X *X
		Y *Y
	}
	sentinel := errors.New("boom")

	cfg := Config{
		MakeX: func(y *Y) *X {
			return &X{}
	tests := []struct {
		name           string
		config         interface{}
		result         interface{}
		expectedErrors []string
		verify         func(t *testing.T, result interface{})
	}{
		{
			name: "constructor error propagation",
			config: struct {
				MakeA func() (*A, error)
			}{
				MakeA: func() (*A, error) {
					return nil, sentinel
				},
			},
			result: &struct {
				A *A
			}{},
			expectedErrors: []string{"MakeA", "boom"},
			verify: func(t *testing.T, result interface{}) {
				res := result.(*struct {
					A *A
				})
				if res.A != nil {
					t.Fatalf("result A should not be populated on constructor failure")
				}
			},
		},
		MakeY: func(x *X) *Y {
			return &Y{}
		{
			name: "missing dependency",
			config: struct {
				MakeB func(*A) (*B, error)
			}{
				MakeB: func(a *A) (*B, error) {
					return &B{a: a}, nil
				},
			},
			result: &struct {
				B *B
			}{},
			expectedErrors: []string{"*di.A", "di.A"},
			verify: func(t *testing.T, result interface{}) {
				res := result.(*struct {
					B *B
				})
				if res.B != nil {
					t.Fatalf("result B should not be populated")
				}
			},
		},
	}

	var res Result
	err := Build(cfg, &res)
	if err == nil {
		t.Fatalf("expected cycle detection error")
	}
	// Both constructors should still be listed as remaining.
	if !strings.Contains(err.Error(), "MakeX") || !strings.Contains(err.Error(), "MakeY") {
		t.Fatalf("expected error to list remaining constructors MakeX and MakeY, got: %v", err)
	}
	if res.X != nil || res.Y != nil {
		t.Fatalf("cycle should prevent any construction; got X=%v Y=%v", res.X, res.Y)
	}
}

// Ensure that providing pre-supplied value satisfies dependency without constructor for it.
func TestBuildWithPreSuppliedValue(t *testing.T) {
	type A struct {
		v int
	}
	type B struct {
		a *A
	}

	type Config struct {
		// Only constructor for B; A provided directly in config.
		A  *A
		MB func(*A) (*B, error)
	}
	type Result struct {
		A *A
		B *B
	}

	cfg := Config{
		A: &A{v: 42},
		MB: func(a *A) (*B, error) {
			return &B{a: a}, nil
		{
			name: "cycle detection",
			config: struct {
				MakeX func(*Y) *X
				MakeY func(*X) *Y
			}{
				MakeX: func(y *Y) *X {
					return &X{}
				},
				MakeY: func(x *X) *Y {
					return &Y{}
				},
			},
			result: &struct {
				X *X
				Y *Y
			}{},
			expectedErrors: []string{"MakeX", "MakeY"},
			verify: func(t *testing.T, result interface{}) {
				res := result.(*struct {
					X *X
					Y *Y
				})
				if res.X != nil || res.Y != nil {
					t.Fatalf("cycle should prevent any construction; got X=%v Y=%v", res.X, res.Y)
				}
			},
		},
	}

	var res Result
	err := Build(cfg, &res)
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if res.A == nil || res.A.v != 42 {
		t.Fatalf("expected pre-supplied A (42), got %+v", res.A)
	}
	if res.B == nil || res.B.a != res.A {
		t.Fatalf("expected B referencing A, got %+v", res.B)
	}
}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			err := Build(tt.config, tt.result)
			if err == nil {
				t.Fatalf("expected error")
			}

func TestTypeAlias(t *testing.T) {
	type ANum int
	type Config struct {
		A ANum
		B int
	}
			errorStr := err.Error()
			var foundError bool
			for _, expectedErr := range tt.expectedErrors {
				if strings.Contains(errorStr, expectedErr) {
					foundError = true
					break
				}
			}
			if !foundError {
				t.Fatalf("expected error to contain one of %v, got: %v", tt.expectedErrors, err)
			}

	type Result struct {
		A ANum
	}
	var res Result
	err := Build(Config{3, 4}, &res)
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if res.A != 3 {
		t.Fatalf("expected A=3, got %v", res.A)
			tt.verify(t, tt.result)
		})
	}
}

func TestReferenceConfig(t *testing.T) {
	type NestedConfig struct {
		OtherSetting   bool
		NestedDecision func(c NestedConfig) uint
	}

	type Config struct {
		NestedConfig
		SomeSetting bool
		Inner       func(c Config) int
	}
func TestNewFunction(t *testing.T) {
	type ANum int

	type Result struct {
		A int
		B uint
	}
	var res Result
	err := Build(Config{
		SomeSetting: true,
		Inner: func(c Config) int {
			if c.SomeSetting {
				return 1
			}
			return 0
	tests := []struct {
		name     string
		config   interface{}
		expected interface{}
	}{
		{
			name: "struct result",
			config: struct {
				A int
			}{A: 42},
			expected: struct {
				A int
			}{A: 42},
		},
		NestedConfig: NestedConfig{
			OtherSetting: true,
			NestedDecision: func(c NestedConfig) uint {
				if c.OtherSetting {
					return 1
				}
				return 0
			},
		{
			name:     "primitive type result",
			config:   struct{ A int }{A: 42},
			expected: 42,
		},
	}, &res)
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if res.A != 1 {
		t.Fatalf("expected A=1, got %v", res.A)
	}
	if res.B != 1 {
		t.Fatalf("expected B=1, got %v", res.A)
	}
}

func TestNew(t *testing.T) {
	type Config struct {
		A int
	}

	type Result struct {
		A int
	}

	cfg := Config{A: 42}
	res, err := New[Result](cfg)
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if res.A != 42 {
		t.Fatalf("expected A=42, got %v", res.A)
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			switch expected := tt.expected.(type) {
			case struct{ A int }:
				res, err := New[struct{ A int }](tt.config)
				if err != nil {
					t.Fatalf("unexpected error: %v", err)
				}
				if res.A != expected.A {
					t.Fatalf("expected A=%d, got %v", expected.A, res.A)
				}
			case int:
				res, err := New[int](tt.config)
				if err != nil {
					t.Fatalf("unexpected error: %v", err)
				}
				if res != expected {
					t.Fatalf("expected %d, got %v", expected, res)
				}
			}
		})
	}
}

func TestSpecificTypes(t *testing.T) {
	type Config struct {
		A int
	}

	cfg := Config{A: 42}
	res, err := New[int](cfg)
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if res != 42 {
		t.Fatalf("expected A=42, got %v", res)
func TestBuildPrimitiveTypes(t *testing.T) {
	tests := []struct {
		name     string
		config   interface{}
		expected int
	}{
		{
			name:     "build primitive directly",
			config:   struct{ A int }{A: 42},
			expected: 42,
		},
	}

	var res2 int
	err = Build(Config{A: 42}, &res2)
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if res2 != 42 {
		t.Fatalf("expected A=42, got %v", res2)
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			var res int
			err := Build(tt.config, &res)
			if err != nil {
				t.Fatalf("unexpected error: %v", err)
			}
			if res != tt.expected {
				t.Fatalf("expected %d, got %v", tt.expected, res)
			}
		})
	}
}