diff --git a/pkg/internal/test/structures.go b/pkg/internal/test/structures.go index 57273f9..7d8f569 100644 --- a/pkg/internal/test/structures.go +++ b/pkg/internal/test/structures.go @@ -59,7 +59,12 @@ func NewRootEmptyHasInfoPtr() *RootEmptyHasInfo { } func (r RootEmptyHasInfo) Info() model.Info { - return model.ComponentInfo("root") + return model.ComponentInfo( + "root description", + "root technology", + "root tag 1", + "root tag 2", + ) } type RootEmptyPtrHasInfo struct{} @@ -73,7 +78,12 @@ func NewRootEmptyPtrHasInfoPtr() *RootEmptyPtrHasInfo { } func (r *RootEmptyPtrHasInfo) Info() model.Info { - return model.ComponentInfo("root") + return model.ComponentInfo( + "root description", + "root technology", + "root tag 1", + "root tag 2", + ) } type RootWithPublicPointerToPublicComponent struct { diff --git a/pkg/scraper/scraper.go b/pkg/scraper/scraper.go index 0393e97..f19e594 100644 --- a/pkg/scraper/scraper.go +++ b/pkg/scraper/scraper.go @@ -92,8 +92,6 @@ func (s *Scraper) scrap( return } - v = normalize(v) - if !s.isScrappable(v) { return } @@ -158,6 +156,8 @@ func (s *Scraper) getInfoFromInterface(v reflect.Value) (model.Info, bool) { var ok bool if v.CanAddr() { + // it allows accessing new pointer by the interface + v = reflect.NewAt(v.Type(), unsafe.Pointer(v.UnsafeAddr())).Elem() // v.Addr() instead of v supports both value and pointer receiver info, ok = v.Addr().Interface().(model.HasInfo) } else if v.CanInterface() { @@ -189,19 +189,6 @@ func (s *Scraper) getInfoFromRules(v reflect.Value) (model.Info, bool) { return model.Info{}, false } -func normalize(v reflect.Value) reflect.Value { - if !v.CanAddr() { - return v - } - - // supports unexported fields - if !v.CanInterface() { - v = reflect.NewAt(v.Type(), unsafe.Pointer(v.UnsafeAddr())).Elem() - } - - return v -} - func componentID(v reflect.Value) string { id := fmt.Sprintf("%s.%s", valuePackage(v), v.Type().Name()) return internal.Hash(id) diff --git a/pkg/scraper/scraper_test.go b/pkg/scraper/scraper_test.go index c254729..0b9a60d 100644 --- a/pkg/scraper/scraper_test.go +++ b/pkg/scraper/scraper_test.go @@ -15,12 +15,59 @@ const ( testPKG = "github.com/krzysztofreczek/go-structurizr/pkg/internal/test" ) -// todo: components matching rule -// todo: scraped info from interface -// todo: scraped info from matching rule -// todo: package matching +func TestScraper_Scrap_package_matching(t *testing.T) { -func TestScraper_Scrap(t *testing.T) { + var tests = []struct { + name string + structure interface{} + packages []string + expectedNumberOfComponents int + }{ + { + name: "structure within given package", + structure: test.NewRootEmptyHasInfo(), + packages: []string{ + "github.com/krzysztofreczek/go-structurizr/pkg/internal/test", + }, + expectedNumberOfComponents: 1, + }, + { + name: "structure out of given package", + structure: test.NewRootEmptyHasInfo(), + packages: []string{ + "github.com/krzysztofreczek/go-structurizr/pkg/foo", + }, + expectedNumberOfComponents: 0, + }, + { + name: "structure within one of given packages", + structure: test.NewRootEmptyHasInfo(), + packages: []string{ + "github.com/krzysztofreczek/go-structurizr/pkg/foo", + "github.com/krzysztofreczek/go-structurizr/pkg/internal/test", + }, + expectedNumberOfComponents: 1, + }, + { + name: "structure within iven package prefix", + structure: test.NewRootEmptyHasInfo(), + packages: []string{ + "github.com/krzysztofreczek/go-structurizr/pkg", + }, + expectedNumberOfComponents: 1, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + c := scraper.NewConfiguration(tt.packages...) + s := scraper.NewScraper(c) + result := s.Scrap(tt.structure) + require.Len(t, result.Components, tt.expectedNumberOfComponents) + }) + } +} + +func TestScraper_Scrap_has_info_interface(t *testing.T) { c := scraper.NewConfiguration( testPKG, ) @@ -513,13 +560,170 @@ func TestScraper_Scrap(t *testing.T) { t.Run(tt.name, func(t *testing.T) { s := scraper.NewScraper(c) result := s.Scrap(tt.structure) - requireEqualComponents(t, tt.expectedComponentIDs, result.Components) + requireEqualComponentIDs(t, tt.expectedComponentIDs, result.Components) requireEqualRelations(t, tt.expectedRelations, result.Relations) }) } } -func requireEqualComponents( +func TestScraper_Scrap_has_info_interface_component_info(t *testing.T) { + c := scraper.NewConfiguration( + testPKG, + ) + var tests = []struct { + name string + structure interface{} + expectedComponents map[string]model.Component + }{ + { + name: "pointer to empty root that implements HasInfo interface", + structure: test.NewRootEmptyHasInfoPtr(), + expectedComponents: map[string]model.Component{ + componentID("RootEmptyHasInfo"): { + ID: componentID("RootEmptyHasInfo"), + Kind: "component", + Name: "test.RootEmptyHasInfo", + Description: "root description", + Technology: "root technology", + Tags: []string{"root tag 1", "root tag 2"}, + }, + }, + }, + { + name: "pointer to empty root that pointer implements HasInfo interface", + structure: test.NewRootEmptyPtrHasInfoPtr(), + expectedComponents: map[string]model.Component{ + componentID("RootEmptyPtrHasInfo"): { + ID: componentID("RootEmptyPtrHasInfo"), + Kind: "component", + Name: "test.RootEmptyPtrHasInfo", + Description: "root description", + Technology: "root technology", + Tags: []string{"root tag 1", "root tag 2"}, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + s := scraper.NewScraper(c) + result := s.Scrap(tt.structure) + requireEqualComponents(t, tt.expectedComponents, result.Components) + }) + } +} + +func TestScraper_Scrap_rules(t *testing.T) { + c := scraper.NewConfiguration( + testPKG, + ) + + ruleDefaultMatchAll, err := scraper.NewRule(). + WithApplyFunc(func() model.Info { + return model.ComponentInfo( + "match-all description", + "match-all technology", + "match-all tag 1", + "match-all tag 2", + ) + }). + Build() + require.NoError(t, err) + + ruleMatchPublicComponent, err := scraper.NewRule(). + WithNameRegexp("^PublicComponent$"). + WithApplyFunc(func() model.Info { + return model.ComponentInfo( + "match-pc description", + "match-pc technology", + "match-pc tag 1", + "match-pc tag 2", + ) + }). + Build() + require.NoError(t, err) + + ruleMatchPublicComponentInAnotherPackage, err := scraper.NewRule(). + WithPkgRegexps("^github.com/krzysztofreczek/go-structurizr/pkg/foo$"). + WithNameRegexp("^PublicComponent$"). + WithApplyFunc(func() model.Info { + return model.ComponentInfo() + }). + Build() + require.NoError(t, err) + + var tests = []struct { + name string + structure interface{} + rules []scraper.Rule + expectedComponents map[string]model.Component + }{ + { + name: "no rules", + structure: test.NewRootEmpty(), + rules: make([]scraper.Rule, 0), + expectedComponents: map[string]model.Component{}, + }, + { + name: "default match-all rule", + structure: test.NewRootWithPublicPointerToPublicComponent(), + rules: []scraper.Rule{ruleDefaultMatchAll}, + expectedComponents: map[string]model.Component{ + componentID("RootWithPublicPointerToPublicComponent"): { + ID: componentID("RootWithPublicPointerToPublicComponent"), + Kind: "component", + Name: "test.RootWithPublicPointerToPublicComponent", + Description: "match-all description", + Technology: "match-all technology", + Tags: []string{"match-all tag 1", "match-all tag 2"}, + }, + componentID("PublicComponent"): { + ID: componentID("PublicComponent"), + Kind: "component", + Name: "test.PublicComponent", + Description: "match-all description", + Technology: "match-all technology", + Tags: []string{"match-all tag 1", "match-all tag 2"}, + }, + }, + }, + { + name: "match-public-component rule", + structure: test.NewRootWithPublicPointerToPublicComponent(), + rules: []scraper.Rule{ruleMatchPublicComponent}, + expectedComponents: map[string]model.Component{ + componentID("PublicComponent"): { + ID: componentID("PublicComponent"), + Kind: "component", + Name: "test.PublicComponent", + Description: "match-pc description", + Technology: "match-pc technology", + Tags: []string{"match-pc tag 1", "match-pc tag 2"}, + }, + }, + }, + { + name: "match-public-component-in-another-package rule", + structure: test.NewRootWithPublicPointerToPublicComponent(), + rules: []scraper.Rule{ruleMatchPublicComponentInAnotherPackage}, + expectedComponents: map[string]model.Component{}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + s := scraper.NewScraper(c) + for _, r := range tt.rules { + err := s.RegisterRule(r) + require.NoError(t, err) + } + + result := s.Scrap(tt.structure) + requireEqualComponents(t, tt.expectedComponents, result.Components) + }) + } +} + +func requireEqualComponentIDs( t *testing.T, expectedComponentIDs map[string]struct{}, actualComponents map[string]model.Component, @@ -531,6 +735,19 @@ func requireEqualComponents( } } +func requireEqualComponents( + t *testing.T, + expectedComponents map[string]model.Component, + actualComponents map[string]model.Component, +) { + require.Len(t, actualComponents, len(expectedComponents)) + for id, expectedComponent := range expectedComponents { + actualComponent, contains := actualComponents[id] + require.True(t, contains, "actual components: %+v", actualComponents) + require.Equal(t, expectedComponent, actualComponent) + } +} + func requireEqualRelations( t *testing.T, expectedRelations map[string][]string,