Skip to content

Commit e69e0ed

Browse files
committed
Improving error handling & test coverage
1 parent 2d8fb67 commit e69e0ed

3 files changed

Lines changed: 719 additions & 3 deletions

File tree

application.go

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -437,12 +437,25 @@ func (app *StdApplication) resolveInterfaceBasedDependencies(
437437
) error {
438438
for _, dep := range dependencies {
439439
// Skip if not interface-based or already satisfied
440-
if !dep.MatchByInterface || dep.SatisfiesInterface == nil ||
441-
dep.SatisfiesInterface.Kind() != reflect.Interface ||
442-
requiredServices[dep.Name] != nil {
440+
if !dep.MatchByInterface || requiredServices[dep.Name] != nil {
443441
continue
444442
}
445443

444+
// Check for invalid interface configuration
445+
if dep.SatisfiesInterface == nil {
446+
if dep.Required {
447+
return fmt.Errorf("invalid interface configuration for required service '%s' in module '%s': SatisfiesInterface is nil (hint: use reflect.TypeOf((*InterfaceName)(nil)).Elem())", dep.Name, moduleName)
448+
}
449+
continue // Skip optional services with invalid interface config
450+
}
451+
452+
if dep.SatisfiesInterface.Kind() != reflect.Interface {
453+
if dep.Required {
454+
return fmt.Errorf("invalid interface configuration for required service '%s' in module '%s': SatisfiesInterface is not an interface type", dep.Name, moduleName)
455+
}
456+
continue // Skip optional services with invalid interface config
457+
}
458+
446459
matchedService, matchedServiceName := app.findServiceByInterface(dep)
447460

448461
if matchedService != nil {
Lines changed: 192 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,192 @@
1+
package modular
2+
3+
import (
4+
"context"
5+
"database/sql"
6+
"reflect"
7+
"testing"
8+
9+
"github.com/stretchr/testify/assert"
10+
"github.com/stretchr/testify/require"
11+
_ "modernc.org/sqlite" // Import pure Go SQLite driver for testing
12+
)
13+
14+
// DatabaseExecutor matches the user's interface from the problem description
15+
type DatabaseExecutor interface {
16+
ExecContext(ctx context.Context, query string, args ...interface{}) (sql.Result, error)
17+
QueryContext(ctx context.Context, query string, args ...interface{}) (*sql.Rows, error)
18+
QueryRowContext(ctx context.Context, query string, args ...interface{}) *sql.Row
19+
PrepareContext(ctx context.Context, query string) (*sql.Stmt, error)
20+
BeginTx(ctx context.Context, opts *sql.TxOptions) (*sql.Tx, error)
21+
}
22+
23+
// Ensure that *sql.DB implements our interface
24+
var _ DatabaseExecutor = (*sql.DB)(nil)
25+
26+
// MockDatabaseService simulates what the database module provides
27+
type MockDatabaseService interface {
28+
DB() *sql.DB
29+
Close() error
30+
}
31+
32+
// mockDatabaseServiceImpl simulates the lazy wrapper that the database module uses
33+
type mockDatabaseServiceImpl struct {
34+
db *sql.DB
35+
}
36+
37+
func (m *mockDatabaseServiceImpl) DB() *sql.DB {
38+
return m.db
39+
}
40+
41+
func (m *mockDatabaseServiceImpl) Close() error {
42+
if m.db != nil {
43+
return m.db.Close()
44+
}
45+
return nil
46+
}
47+
48+
// TestInterfaceMatchingCore demonstrates the core issue with interface matching
49+
func TestInterfaceMatchingCore(t *testing.T) {
50+
// Create a mock database service (simulating what the database module provides)
51+
db, err := sql.Open("sqlite", ":memory:")
52+
require.NoError(t, err)
53+
defer db.Close()
54+
55+
mockService := &mockDatabaseServiceImpl{db: db}
56+
57+
// Test 1: Check if *sql.DB implements DatabaseExecutor (it should)
58+
expectedType := reflect.TypeOf((*DatabaseExecutor)(nil)).Elem()
59+
sqlDBType := reflect.TypeOf((*sql.DB)(nil))
60+
61+
assert.True(t, sqlDBType.Implements(expectedType),
62+
"*sql.DB should implement DatabaseExecutor interface")
63+
64+
// Test 2: Check if mockDatabaseServiceImpl implements DatabaseExecutor (it should NOT)
65+
mockServiceType := reflect.TypeOf((*mockDatabaseServiceImpl)(nil))
66+
assert.False(t, mockServiceType.Implements(expectedType),
67+
"mockDatabaseServiceImpl should NOT implement DatabaseExecutor interface")
68+
69+
// Test 3: Check if mockDatabaseServiceImpl implements MockDatabaseService (it should)
70+
mockDBServiceType := reflect.TypeOf((*MockDatabaseService)(nil)).Elem()
71+
assert.True(t, mockServiceType.Implements(mockDBServiceType),
72+
"mockDatabaseServiceImpl should implement MockDatabaseService interface")
73+
74+
// Test 4: Demonstrate the workaround - extract the *sql.DB from the service
75+
actualDB := mockService.DB()
76+
actualDBType := reflect.TypeOf(actualDB)
77+
assert.True(t, actualDBType.Implements(expectedType),
78+
"The *sql.DB extracted from the service should implement DatabaseExecutor")
79+
80+
t.Log("✅ Core issue demonstrated:")
81+
t.Log(" - *sql.DB implements DatabaseExecutor ✓")
82+
t.Log(" - Database service wrapper does NOT implement DatabaseExecutor ✗")
83+
t.Log(" - But wrapper.DB() returns *sql.DB which does implement DatabaseExecutor ✓")
84+
}
85+
86+
// TestServiceDependencyMatching simulates the service dependency resolution
87+
func TestServiceDependencyMatching(t *testing.T) {
88+
// Create a test config that won't fail environment parsing
89+
config := struct {
90+
TestKey string `yaml:"test_key" default:"test_value"`
91+
}{}
92+
93+
// Create a test application with proper config
94+
app := NewStdApplication(
95+
NewStdConfigProvider(config),
96+
&logger{t},
97+
)
98+
99+
// Create a mock database module that provides a wrapper service
100+
mockDBModule := &MockDatabaseModule{name: "mock-database"}
101+
app.RegisterModule(mockDBModule)
102+
103+
// Create a module that tries to use MatchByInterface=true with WRONG reflection syntax
104+
failingModule := &FailingTestModule{name: "failing-module"}
105+
app.RegisterModule(failingModule)
106+
107+
// This should fail with MatchByInterface=true due to the incorrect reflect.TypeOf pattern
108+
err := app.Init()
109+
assert.Error(t, err, "Should fail when using incorrect reflect.TypeOf pattern")
110+
if err != nil {
111+
t.Logf("Expected failure: %v", err)
112+
// The error should be about invalid interface configuration due to nil SatisfiesInterface
113+
assert.Contains(t, err.Error(), "invalid interface configuration", "Error should mention invalid interface configuration")
114+
assert.Contains(t, err.Error(), "SatisfiesInterface is nil", "Error should mention SatisfiesInterface is nil")
115+
assert.Contains(t, err.Error(), "hint: use reflect.TypeOf((*InterfaceName)(nil)).Elem()", "Error should provide usage hint")
116+
}
117+
}
118+
119+
// MockDatabaseModule simulates the database module
120+
type MockDatabaseModule struct {
121+
name string
122+
}
123+
124+
func (m *MockDatabaseModule) Name() string {
125+
return m.name
126+
}
127+
128+
func (m *MockDatabaseModule) Init(_ Application) error {
129+
return nil
130+
}
131+
132+
func (m *MockDatabaseModule) Dependencies() []string {
133+
return nil
134+
}
135+
136+
func (m *MockDatabaseModule) ProvidesServices() []ServiceProvider {
137+
// Create a dummy *sql.DB for testing
138+
db, _ := sql.Open("sqlite", ":memory:")
139+
wrapper := &mockDatabaseServiceImpl{db: db}
140+
141+
return []ServiceProvider{
142+
{
143+
Name: "database.service",
144+
Description: "Database service wrapper",
145+
Instance: wrapper, // Provide wrapper, not *sql.DB directly
146+
},
147+
}
148+
}
149+
150+
func (m *MockDatabaseModule) RequiresServices() []ServiceDependency {
151+
return nil
152+
}
153+
154+
// FailingTestModule tries to use MatchByInterface=true (will fail)
155+
type FailingTestModule struct {
156+
name string
157+
db DatabaseExecutor
158+
}
159+
160+
func (m *FailingTestModule) Name() string {
161+
return m.name
162+
}
163+
164+
func (m *FailingTestModule) Init(_ Application) error {
165+
return nil
166+
}
167+
168+
func (m *FailingTestModule) Dependencies() []string {
169+
return []string{"mock-database"}
170+
}
171+
172+
func (m *FailingTestModule) ProvidesServices() []ServiceProvider {
173+
return nil
174+
}
175+
176+
func (m *FailingTestModule) RequiresServices() []ServiceDependency {
177+
return []ServiceDependency{
178+
{
179+
Name: "nonexistent.service", // Change to a service name that doesn't exist
180+
Required: true,
181+
MatchByInterface: true, // This will fail due to incorrect reflect pattern
182+
SatisfiesInterface: reflect.TypeOf((DatabaseExecutor)(nil)), // ❌ WRONG - returns nil
183+
},
184+
}
185+
}
186+
187+
func (m *FailingTestModule) Constructor() ModuleConstructor {
188+
return func(app Application, services map[string]any) (Module, error) {
189+
// This won't be called because dependency resolution will fail
190+
return m, nil
191+
}
192+
}

0 commit comments

Comments
 (0)