diff --git a/http/availability/printer.go b/http/availability/printer.go index e9c0beb..e773401 100644 --- a/http/availability/printer.go +++ b/http/availability/printer.go @@ -58,10 +58,7 @@ func DecodeOutput(enabled bool) func(*Printer) { return func(p *Printer) { if enabled { p.decoder = func(origin string) string { - decoded, err := url.PathUnescape(origin) - if err != nil { - return origin - } + decoded, _ := url.PathUnescape(origin) return decoded } } diff --git a/http/availability/printer_test.go b/http/availability/printer_test.go index 678e9a7..a444036 100644 --- a/http/availability/printer_test.go +++ b/http/availability/printer_test.go @@ -71,6 +71,7 @@ func TestPrinter(t *testing.T) { {StatusCode: http.StatusFound, Location: "http://howilive.ru/en/", Redirect: "https://howilive.ru/en/"}, {StatusCode: http.StatusProcessing, Location: "https://twitter.com/ikamilsk"}, + {Internal: true, StatusCode: http.StatusOK, Location: "https://kamil.samigullin.info/"}, }, }, { @@ -81,6 +82,53 @@ func TestPrinter(t *testing.T) { {StatusCode: http.StatusFound, Location: "http://howilive.ru/en/", Redirect: "https://howilive.ru/en/"}, {StatusCode: http.StatusProcessing, Location: "https://twitter.com/ikamilsk"}, + {Internal: true, StatusCode: http.StatusOK, Location: "https://kamil.samigullin.info/en/"}, + }, + }, + }} + close(data) + var pipe <-chan availability.Site = data + m.On("Sites").Return(pipe) + return m + }, + assert.NoError, + "[200] https://kamil.samigullin.info/", + }, + { + "extra configured", + func() *availability.Printer { + return availability.NewPrinter( + availability.ColorizeOutput(true), + availability.DecodeOutput(true), + availability.HideError(true), + availability.HideRedirect(true), + availability.OutputForPrinting(buf), + ) + }, + func() availability.Reporter { + m := &PrinterMock{} + data := make(chan availability.Site, 1) + data <- availability.Site{Pages: []*availability.Page{ + { + &availability.Link{StatusCode: http.StatusOK, Location: "https://kamil.samigullin.info/en/"}, + []availability.Link{ + {StatusCode: http.StatusServiceUnavailable, Location: "https://github.com/kamilsk"}, + {StatusCode: http.StatusForbidden, Location: "https://www.linkedin.com/in/kamilsk"}, + {StatusCode: http.StatusFound, + Location: "http://howilive.ru/en/", Redirect: "https://howilive.ru/en/"}, + {StatusCode: http.StatusProcessing, Location: "https://twitter.com/ikamilsk"}, + {Internal: true, StatusCode: http.StatusOK, Location: "https://kamil.samigullin.info/"}, + }, + }, + { + Link: &availability.Link{StatusCode: http.StatusOK, Location: "https://kamil.samigullin.info/"}, + Links: []availability.Link{ + {StatusCode: http.StatusServiceUnavailable, Location: "https://github.com/kamilsk"}, + {StatusCode: http.StatusForbidden, Location: "https://www.linkedin.com/in/kamilsk"}, + {StatusCode: http.StatusFound, + Location: "http://howilive.ru/en/", Redirect: "https://howilive.ru/en/"}, + {StatusCode: http.StatusProcessing, Location: "https://twitter.com/ikamilsk"}, + {Internal: true, StatusCode: http.StatusOK, Location: "https://kamil.samigullin.info/en/"}, }, }, }} diff --git a/http/availability/report.go b/http/availability/report.go index e3f19d0..80e0eeb 100644 --- a/http/availability/report.go +++ b/http/availability/report.go @@ -33,7 +33,6 @@ type Report struct { // For prepares report builder for passed websites' URLs. func (r *Report) For(rawURLs []string) *Report { r.sites = make([]*Site, 0, len(rawURLs)) - r.ready = make(chan Site, len(rawURLs)) for _, rawURL := range rawURLs { r.sites = append(r.sites, NewSite(rawURL)) } @@ -42,31 +41,23 @@ func (r *Report) For(rawURLs []string) *Report { // Fill starts to fetch sites and prepared them for reading. func (r *Report) Fill() *Report { - wg := &sync.WaitGroup{} + r.ready = make(chan Site, len(r.sites)) for _, site := range r.sites { - wg.Add(1) - go func(site *Site) { - var copied Site - copied.Name = site.Name - defer wg.Done() - defer func() { r.ready <- copied }() - defer errors.Recover(&copied.Error) - site.Error = site.Fetch(r.crawler) - { - copied = *site - pages := make([]*Page, 0, len(site.Pages)) - for _, page := range site.Pages { - page := *page - pages = append(pages, &page) - links := make([]Link, len(page.Links)) - copy(links, page.Links) - page.Links = links - } - copied.Pages = pages + site.Error = site.Fetch(r.crawler) + { + copied := *site + pages := make([]*Page, 0, len(site.Pages)) + for _, page := range site.Pages { + page := *page + pages = append(pages, &page) + links := make([]Link, len(page.Links)) + copy(links, page.Links) + page.Links = links } - }(site) + copied.Pages = pages + r.ready <- copied + } } - wg.Wait() close(r.ready) return r } @@ -157,24 +148,14 @@ func (s *Site) listen(events <-chan event) { barrier := make(map[*Page]map[*Link]struct{}) s.Pages = make([]*Page, 0, len(pages)) for location, page := range pages { - link, found := links[location] - if !found { - panic(errors.Errorf("panic: not consistent fetch result. link %q not found", location)) - } - page.Link = link + page.Link = links[location] s.Pages = append(s.Pages, page) barrier[page] = make(map[*Link]struct{}) } for _, linkAndPage := range linkToPage { linkLocation, pageLocation := linkAndPage[0], linkAndPage[1] - link, found := links[linkLocation] - if !found { - panic(errors.Errorf("panic: not consistent fetch result. link %q not found", linkLocation)) - } - page, found := pages[pageLocation] - if !found { - panic(errors.Errorf("panic: not consistent fetch result. page %q not found", pageLocation)) - } + link := links[linkLocation] + page := pages[pageLocation] if _, exists := barrier[page][link]; !exists { barrier[page][link] = struct{}{} { @@ -211,15 +192,9 @@ func hostOrRawURL(u *url.URL, raw string) string { } func hasSameHost(link1, link2 string) bool { - u1, err := url.Parse(link1) - if err != nil { - return false - } - u2, err := url.Parse(link2) - if err != nil { - return false - } - return u1.Host == u2.Host + u1, _ := url.Parse(link1) + u2, _ := url.Parse(link2) + return u1 != nil && u2 != nil && u1.Host == u2.Host } type event interface { diff --git a/http/availability/report_test.go b/http/availability/report_test.go index 3ad355d..5e4248f 100644 --- a/http/availability/report_test.go +++ b/http/availability/report_test.go @@ -104,3 +104,59 @@ func TestReporter(t *testing.T) { }) } } + +func TestReporter_handlePanic(t *testing.T) { + tests := []struct { + name string + rawURLs []string + reporter func() *availability.Report + expected string + }{ + { + "unexpected event", + []string{"http://test.dev/"}, + func() *availability.Report { + crawler := &CrawlerMock{shift: func(to availability.EventBus) { + type unknown struct{ availability.ProblemEvent } + to <- unknown{availability.ProblemEvent{Message: "bad url", Context: ":bad"}} + close(to) + }} + crawler.On("Visit", "http://test.dev/", mock.Anything).Return(nil) + report := availability.NewReport(availability.CrawlerForSites(crawler)) + return report + }, + "panic: unexpected event type availability_test.unknown", + }, + { + "not consistent fetch result", + []string{"http://test.dev/"}, + func() *availability.Report { + crawler := &CrawlerMock{shift: func(to availability.EventBus) { + to <- availability.ResponseEvent{StatusCode: http.StatusOK, Location: "http://test.dev/"} + to <- availability.WalkEvent{Page: "http://test.dev/without-response/", Href: "http://test.dev/"} + close(to) + }} + crawler.On("Visit", "http://test.dev/", mock.Anything).Return(nil) + report := availability.NewReport(availability.CrawlerForSites(crawler)) + return report + }, + "runtime error: invalid memory address or nil pointer dereference", + }, + } + for _, test := range tests { + tc := test + t.Run(test.name, func(t *testing.T) { + assert.Panics(t, func() { + defer func() { + if r := recover(); r != nil { + err, is := r.(error) + assert.True(t, is) + assert.EqualError(t, err, tc.expected) + panic(r) + } + }() + tc.reporter().For(tc.rawURLs).Fill() + }) + }) + } +} diff --git a/main.go b/main.go index d1fa4c9..3469725 100644 --- a/main.go +++ b/main.go @@ -7,6 +7,7 @@ import ( "runtime" "github.com/kamilsk/check/cmd" + "github.com/kamilsk/check/errors" "github.com/spf13/cobra" ) @@ -28,6 +29,16 @@ type application struct { // Run executes the application logic. func (app application) Run() { + var err error + defer func() { + errors.Recover(&err) + if err != nil { + // so, when `issue` project will be ready + // I have to integrate it to open GitHub issues + // with stack trace from terminal + app.Shutdown(failed) + } + }() app.Cmd.AddCommand(&cobra.Command{ Use: "version", Short: "Show application version", @@ -38,10 +49,7 @@ func (app application) Run() { }, Version: version, }) - if err := app.Cmd.Execute(); err != nil { - // so, when `issue` project will be ready - // I have to integrate it to open GitHub issues - // with stack trace from terminal + if err = app.Cmd.Execute(); err != nil { app.Shutdown(failed) } app.Shutdown(success) diff --git a/main_test.go b/main_test.go index 19fe0f9..36605ff 100644 --- a/main_test.go +++ b/main_test.go @@ -48,6 +48,19 @@ func TestApplication_Run(t *testing.T) { }, failed, }, + { + "panicked run", + func() interface { + AddCommand(...*cobra.Command) + Execute() error + } { + cmd := &CmdMock{} + cmd.On("AddCommand", mock.Anything) + cmd.On("Execute").Run(func(mock.Arguments) { panic("something unexpected") }) + return cmd + }, + failed, + }, } for _, test := range tests { tc := test