// callGetAnalyzerInfo requests the categories for the specified analyzer and puts them onto the // channel provided. If anything goes wrong, it returns the empty set. func callGetAnalyzerInfo(analyzer string, out chan<- serviceInfo) { httpClient := getHTTPClient(analyzer) var catResp rpcpb.GetCategoryResponse var stageResp rpcpb.GetStageResponse var cats strset.Set var stage contextpb.Stage // TODO(ciera): Maybe we should just combine these into one call... err := httpClient.Call("/AnalyzerService/GetCategory", &rpcpb.GetCategoryRequest{}, &catResp) if err != nil { log.Printf("Could not get categories from %s: %v", analyzer, err) cats = strset.New() } else { cats = strset.New(catResp.Category...) } err = httpClient.Call("/AnalyzerService/GetStage", &rpcpb.GetStageRequest{}, &stageResp) if err != nil { log.Printf("Could not get stage from %s: %v", analyzer, err) cats = strset.New() } else { stage = *stageResp.Stage } out <- serviceInfo{ analyzer: analyzer, categories: cats, stage: stage, } }
func TestGetServiceInfo(t *testing.T) { addr2, cleanup, err := testutil.CreatekRPCTestServer(&fakeDispatcher{[]string{"Foo", "Bar"}, nil}, "AnalyzerService") if err != nil { t.Fatalf("Registering analyzer service failed: %v", err) } defer cleanup() addr0, cleanup, err := testutil.CreatekRPCTestServer(&fakeDispatcher{nil, nil}, "AnalyzerService") if err != nil { t.Fatalf("Registering analyzer service failed: %v", err) } defer cleanup() addre, cleanup, err := testutil.CreatekRPCTestServer(&errDispatcher{}, "AnalyzerService") if err != nil { t.Fatalf("Registering analyzer service failed: %v", err) } defer cleanup() addrp, cleanup, err := testutil.CreatekRPCTestServer(&panicDispatcher{}, "AnalyzerService") if err != nil { t.Fatalf("Registering analyzer service failed: %v", err) } defer cleanup() tests := []struct { addrs []string result map[string]strset.Set }{ {[]string{addr0}, map[string]strset.Set{addr0: nil}}, {[]string{addr2}, map[string]strset.Set{addr2: strset.New("Foo", "Bar")}}, {[]string{addr0, addr2}, map[string]strset.Set{addr2: strset.New("Foo", "Bar"), addr0: nil}}, {[]string{addrp, addr2}, map[string]strset.Set{addr2: strset.New("Foo", "Bar"), addrp: nil}}, {[]string{addre, addr2}, map[string]strset.Set{addr2: strset.New("Foo", "Bar"), addre: nil}}, } for _, test := range tests { driver := NewDriver(test.addrs, nil) info := driver.getAllServiceInfo() if len(test.result) != len(info) { t.Errorf("Incorrect number of results: got %v, want %v", info, test.result) } for addr, expectCats := range test.result { if !strset.Equal(info[strings.TrimPrefix(addr, "http://")].categories.ToSlice(), expectCats.ToSlice()) { t.Errorf("Incorrect categories for %s: got %v, want %v", addr, info[addr].categories, expectCats) } } } }
func TestCallAllAnalyzers(t *testing.T) { dispatcher := &fakeDispatcher{categories: []string{"Foo", "Bar"}, files: []string{"dir1/A.h", "dir1/A.cc"}} addr, cleanup, err := testutil.CreatekRPCTestServer(dispatcher, "AnalyzerService") if err != nil { t.Fatalf("Registering analyzer service failed: %v", err) } defer cleanup() driver := NewTestDriver([]serviceInfo{ serviceInfo{addr, strset.New("Foo", "Bar"), ctxpb.Stage_PRE_BUILD}, }) tests := []struct { files []string categories []string expect []*notepb.Note }{ { // - SomeOtherCategory should be dropped; if it weren't fakeDispatcher would return an error. // - fakeDispatcher produces notes for files we didn't ask about; they should be dropped. []string{"dir1/A.cc"}, []string{"Foo", "SomeOtherCategory"}, []*notepb.Note{ ¬epb.Note{ Category: proto.String("Foo"), Description: proto.String(""), Location: testutil.CreateLocation("dir1/A.cc"), }, }, }, } for _, test := range tests { ctx := &ctxpb.ShipshapeContext{FilePath: test.files} ars := driver.callAllAnalyzers(strset.New(test.categories...), ctx, ctxpb.Stage_PRE_BUILD) var notes []*notepb.Note for _, ar := range ars { notes = append(notes, ar.Note...) if len(ar.Failure) > 0 { t.Errorf("Received failures from analyze call: %v", ar.Failure) } } ok, results := testutil.CheckNoteContainsContent(test.expect, notes) if !ok { t.Errorf("Incorrect notes for categories %v: %s\n got %v, want %v", test.categories, results, notes, test.expect) } } }
// allCats returns the entire set of categories for the driver, across all analyzers func (sd ShipshapeDriver) allCats() strset.Set { var catSet = strset.New() for _, info := range sd.serviceMap { catSet.AddSet(info.categories) } return catSet }
// Analyze will determine which analyzers to run and call them as appropriate. If necessary, it will // also modify the context before calling the analyzers. It recovers from all analyzer panics with a // note that the analyzer failed. func (s analyzerService) Analyze(ctx server.Context, in *rpcpb.AnalyzeRequest) (resp *rpcpb.AnalyzeResponse, err error) { resp = new(rpcpb.AnalyzeResponse) log.Printf("called with: %v", proto.MarshalTextString(in)) log.Print("starting analyzing") var nts []*notepb.Note var errs []*rpcpb.AnalysisFailure defer func() { resp.Note = nts resp.Failure = errs }() orgDir, restore, err := file.ChangeDir(*in.ShipshapeContext.RepoRoot) if err != nil { log.Printf("Internal error before analyzing: %v", err) appendFailure(&errs, "InternalFailure", err) return resp, err } defer func() { if err := restore(); err != nil { log.Printf("could not return back into %s from %s: %v", orgDir, *in.ShipshapeContext.RepoRoot, err) } }() reqCats := strset.New(in.Category...) for _, a := range s.analyzers { if reqCats.Contains(a.Category()) { runAnalyzer(a, in.ShipshapeContext, &nts, &errs) } } log.Printf("finished analyzing, sending back %d notes and %d errors", len(nts), len(errs)) return resp, nil }
func (f fakeDispatcher) Analyze(ctx server.Context, in *rpcpb.AnalyzeRequest) (*rpcpb.AnalyzeResponse, error) { var nts []*notepb.Note // Assert that the analyzer was called with the right categories. if !isSubset(strset.New(in.Category...), strset.New(f.categories...)) { return nil, fmt.Errorf("Category mismatch: got %v, supports %v", in.Category, f.categories) } for _, file := range f.files { nts = append(nts, ¬epb.Note{ Category: proto.String(f.categories[0]), Description: proto.String("Hello world"), Location: testutil.CreateLocation(file), }) } return &rpcpb.AnalyzeResponse{ Note: nts, }, nil }
// filterResults removes any notes where the category is nil, the category is not specified for // the file path by the configuration, or there is no location with a source context. // The config category and internal failure category cannot be turned off. func filterResults(context *contextpb.ShipshapeContext, response *rpcpb.AnalyzeResponse) *rpcpb.AnalyzeResponse { files := strset.New(context.FilePath...) var keep []*notepb.Note for _, note := range response.Note { if note.Category != nil { if note.Location != nil && (note.Location.Path == nil || files.Contains(*note.Location.Path)) { keep = append(keep, note) } } } return &rpcpb.AnalyzeResponse{ Note: keep, Failure: response.Failure, } }
// Run runs the analyzers that this driver knows about on the provided ShipshapeRequest, // taking configuration into account. func (sd ShipshapeDriver) Run(ctx server.Context, in *rpcpb.ShipshapeRequest, out chan<- *rpcpb.ShipshapeResponse) error { var ars []*rpcpb.AnalyzeResponse log.Printf("Received analysis request for event %v, stage %v, categories %v, repo %v", *in.Event, *in.Stage, in.TriggeredCategory, *in.ShipshapeContext.RepoRoot) // However we exit, send back the set of collected AnalyzeResponses // TODO(ciera): we should be streaming back the responses, not sending them all at the end. defer func() { out <- &rpcpb.ShipshapeResponse{ AnalyzeResponse: ars, } }() if in.ShipshapeContext.RepoRoot == nil { return fmt.Errorf("No repo root was set") } root := *in.ShipshapeContext.RepoRoot // cd into the root directory orgDir, restore, err := file.ChangeDir(root) if err != nil { log.Printf("Could not change into directory %s from base %s", root, orgDir) ars = append(ars, generateFailure("Driver setup", fmt.Sprint(err))) return err } defer func() { if err := restore(); err != nil { log.Printf("could not return back into %s from %s: %v", orgDir, root, err) } }() cfg, err := loadConfig(configFilename, *in.Event) if err != nil { log.Print("error loading config") // TODO(collinwinter): attach the error to the config file. ars = append(ars, generateFailure("Driver setup", err.Error())) return err } // Get the list of all categories sd.serviceMap = sd.getAllServiceInfo() allCats := sd.allCats() // Use the triggered categories if specified var desiredCats strset.Set if len(in.TriggeredCategory) > 0 { desiredCats = strset.New(in.TriggeredCategory...) } else if cfg != nil { desiredCats = strset.New(cfg.categories...) if len(desiredCats) > 0 { log.Printf("Running with categories from .shipshape file: %s" + desiredCats.String()) } else if *in.Event != defaults.DefaultEvent { return fmt.Errorf("No categories configured for event %s", *in.Event) } } if len(desiredCats) == 0 { log.Printf("No categories specified, running with default categories: %s", sd.defaultCategories.String()) desiredCats = sd.defaultCategories } // Find out what categories we have available, and remove/warn on the missing ones missingCats := strset.New().AddSet(desiredCats).RemoveSet(allCats) for missing := range missingCats { ars = append(ars, generateFailure(missing, fmt.Sprintf("The triggered category %q could not be found at the locations %v", missing, sd.AnalyzerLocations))) } desiredCats = desiredCats.RemoveSet(missingCats) if len(desiredCats) == 0 { log.Printf("No categories configured to run, doing nothing") return nil } // TODO(ciera): move this global ignore stuff into the CLI processing ignorePaths := []string{} if cfg != nil { ignorePaths = cfg.ignore } // Fill in the file_paths if they are empty in the context context := proto.Clone(in.ShipshapeContext).(*contextpb.ShipshapeContext) context.FilePath, err = retrieveAndFilterFiles(*context.RepoRoot, context.FilePath, ignorePaths) if err != nil { log.Printf("Had problems accessing files: %v", err.Error()) ars = append(ars, generateFailure("Driver setup", fmt.Sprint(err))) return err } if len(context.FilePath) == 0 { log.Print("No files to run on, doing nothing") return nil } // TODO(ciera): rather than pass the stage through here and checking all analyzers, // filter out the stages earlier, when we check categories stage := contextpb.Stage_PRE_BUILD if in.Stage != nil { stage = *in.Stage } log.Printf("Analyzing stage %s", stage.String()) if stage == contextpb.Stage_PRE_BUILD { ars = append(ars, sd.callAllAnalyzers(desiredCats, context, stage)...) } /*else { comps := filepath.Join(*context.RepoRoot, compilationsDir) compUnits, err := findCompilationUnits(comps) log.Printf("Found %d compUnits at %s", len(compUnits), comps) if err != nil { log.Printf("Could not retrieve compilation units: %v", err) ars = append(ars, generateFailure("Driver setup", err.Error())) return nil } for path, compUnit := range compUnits { context.CompilationDetails = &contextpb.CompilationDetails{ CompilationUnit: compUnit, CompilationDescriptionPath: proto.String(path), } log.Printf("Calling services with comp unit at %s", path) ars = append(ars, sd.callAllAnalyzers(desiredCats, context, stage)...) } } */ log.Print("Analysis completed") return nil }
func TestCallAllAnalyzersErrorCases(t *testing.T) { ctx := &ctxpb.ShipshapeContext{FilePath: []string{"dir1/A", "dir2/B"}} tests := []struct { response *rpcpb.AnalyzeResponse expectNotes []*notepb.Note expectFailure []*rpcpb.AnalysisFailure }{ { //analysis had a failure &rpcpb.AnalyzeResponse{ Failure: []*rpcpb.AnalysisFailure{ &rpcpb.AnalysisFailure{ Category: proto.String("Foo"), FailureMessage: proto.String("badbadbad"), }, }, }, nil, []*rpcpb.AnalysisFailure{ &rpcpb.AnalysisFailure{ Category: proto.String("Foo"), FailureMessage: proto.String("badbadbad"), }, }, }, { //analysis had both failure and notes &rpcpb.AnalyzeResponse{ Note: []*notepb.Note{ ¬epb.Note{ Category: proto.String("Foo"), Description: proto.String("A note"), Location: testutil.CreateLocation("dir1/A"), }, ¬epb.Note{ Category: proto.String("Foo"), Description: proto.String("A note"), Location: testutil.CreateLocation("dir1/A"), }, }, Failure: []*rpcpb.AnalysisFailure{ &rpcpb.AnalysisFailure{ Category: proto.String("Foo"), FailureMessage: proto.String("badbadbad"), }, }, }, []*notepb.Note{ ¬epb.Note{ Category: proto.String("Foo"), Description: proto.String("A note"), Location: testutil.CreateLocation("dir1/A"), }, ¬epb.Note{ Category: proto.String("Foo"), Description: proto.String("A note"), Location: testutil.CreateLocation("dir1/A"), }, }, []*rpcpb.AnalysisFailure{ &rpcpb.AnalysisFailure{ Category: proto.String("Foo"), FailureMessage: proto.String("badbadbad"), }, }, }, } for _, test := range tests { addr, cleanup, err := testutil.CreatekRPCTestServer(&fullFakeDispatcher{test.response}, "AnalyzerService") if err != nil { t.Fatalf("Registering analyzer service failed: %v", err) } defer cleanup() driver := NewTestDriver([]serviceInfo{ serviceInfo{addr, strset.New("Foo"), ctxpb.Stage_PRE_BUILD}, }) ars := driver.callAllAnalyzers(strset.New("Foo"), ctx, ctxpb.Stage_PRE_BUILD) var notes []*notepb.Note var failures []*rpcpb.AnalysisFailure for _, ar := range ars { notes = append(notes, ar.Note...) failures = append(failures, ar.Failure...) } ok, results := testutil.CheckNoteContainsContent(test.expectNotes, notes) if !ok { t.Errorf("Incorrect notes for original response %v: %s\n got %v, want %v", test.response, results, notes, test.expectNotes) } ok, results = testutil.CheckFailureContainsContent(test.expectFailure, failures) if !ok { t.Errorf("Incorrect failures for original response %v: %s\n got %v, want %v", test.response, results, failures, test.expectFailure) } } }
func main() { flag.Parse() log.SetFlags(log.Ldate | log.Ltime | log.Lmicroseconds | log.Lshortfile) analyzerList := strings.Split(*analyzers, ",") log.Printf("Waiting for analyzers to become healthy...") healthErrors := service.WaitForAnalyzers(analyzerList) allHealthy := true for addr, err := range healthErrors { if err != nil { log.Printf("Analyzer at %s failed to become healthy: %v", addr, err) allHealthy = false } else { log.Printf("Analyzer at %s registered", addr) } } if allHealthy { log.Printf("All analyzers deemed healthy") } defaultCategories := strset.New( "CheckstyleGoogle", "ErrorProne", "CodeAlert", "JSHint", "PyLint", "go vet") shipshapeService := service.NewDriver(analyzerList, defaultCategories) if *startService { // Start shipshape service s1 := server.Service{Name: serviceName} if err := s1.Register(shipshapeService); err != nil { log.Fatalf("Registering shipshape service failed: %v", err) } addr := fmt.Sprintf(":%d", *servicePort) log.Printf("Starting server endpoint at %q with service name %s\n", addr, serviceName) http.Handle("/", server.Endpoint{&s1}) if err := http.ListenAndServe(addr, nil); err != nil { log.Fatalf("Server startup failed: %v", err) } } else { log.Println("Waiting for stdin. Specify --start_service if you meant to start as a service.") // Read request bytes from stdin requestBytes, err := ioutil.ReadAll(os.Stdin) if err != nil { log.Fatal("failed to read stdin: ", err) } log.Printf("Read shipshape request on stdin [%v bytes]", len(requestBytes)) // Convert bytes from stdin to Shipshape request request := new(rpcpb.ShipshapeRequest) err = proto.Unmarshal(requestBytes, request) if err != nil { log.Fatal("failed to unmarshal shipshape request stream: ", err) } c := make(chan *rpcpb.ShipshapeResponse) go func() { if err := shipshapeService.Run(nil, request, c); err != nil { log.Printf("Failed to run on server: %v", err) } }() log.Print("Sent request to driver") response := <-c log.Printf("Shipshape response: [%s]", response) responseBytes, err := proto.Marshal(response) if err != nil { log.Fatal("failed to marshal shipshape response: ", err) } log.Printf("Writing Shipshape response to stdout") os.Stdout.Write(responseBytes) } }