package main import ( "bufio" "bytes" "context" "fmt" "io" "log" "mime/multipart" "net/http" "os" "time" "gitea.teamortix.com/team-ortix/coverage/cover" ) const ( profilePath = "/go/coverage.out" ) func main() { event := os.Getenv("DRONE_BUILD_EVENT") if event != "push" && event != "pull_request" { log.Printf("DRONE_BUILD EVENT of '%s' skipped. Expecting 'push' or 'pull_request'", event) return } namespace := os.Getenv("DRONE_REPO_NAMESPACE") if namespace == "" { log.Fatalln("could not find DRONE_REPO_NAMESPACE") } project := os.Getenv("DRONE_REPO_NAME") if project == "" { log.Fatalln("could not find DRONE_REPO_NAME") } switch event { case "push": branch := os.Getenv("DRONE_BRANCH") if branch == "" { log.Fatalln("could not find DRONE_BRANCH") } handlePush(namespace, project, branch) case "pull_request": pull := os.Getenv("DRONE_PULL_REQUEST") if pull == "" { log.Fatalln("could not find DRONE_PULL_REQUEST") } handlePullRequest(namespace, project, pull) default: log.Fatalf("invalid branch name: %s\n", event) } } const urlFormat = "https://coverage.teamortix.com/upload/%s/%s/%s/%s" func handlePush(namespace, project, branch string) { tmpl, percentage := getCoverageData() url := fmt.Sprintf(urlFormat, "branch", namespace, project, branch) err := request(url, percentage, tmpl) if err != nil { log.Fatalf("could not make request: %v\n", err) } log.Printf("Successfully made request to %s.\n", url) } func handlePullRequest(namespace, project, pull string) { tmpl, percentage := getCoverageData() url := fmt.Sprintf(urlFormat, "pulls", namespace, project, pull) err := request(url, percentage, tmpl) if err != nil { log.Fatalf("could not make request: %v\n", err) } log.Printf("Successfully made request to %s.\n", url) } // getCoverageData uses `go tool cover` to get the coverage profile. // It expects coverage file `coverage.out` to be present in `/go`. func getCoverageData() (cover.TemplateData, float64) { file, err := os.Open(profilePath) if err != nil { log.Fatalf("could not open cover profiles: %v\n", err) } profiles, err := cover.ParseProfiles(bufio.NewReader(file)) if err != nil { log.Fatalf("could not parse cover profiles: %v\n", err) } tmpl, err := cover.Template(profiles) if err != nil { log.Fatalf("could not parse cover profiles: %v\n", err) } coverage := percentCovered(profiles) return tmpl, coverage } // percentCovered returns, as a percentage, the fraction of the statements in // the profile covered by the test run. // In effect, it reports the coverage of a given source file. func percentCovered(ps []*cover.Profile) float64 { var total, covered int64 for _, p := range ps { for _, b := range p.Blocks { total += int64(b.NumStmt) if b.Count > 0 { covered += int64(b.NumStmt) } } if total == 0 { return 0 } } return float64(covered) / float64(total) } func request(url string, coverage float64, tmpl cover.TemplateData) error { client := &http.Client{Timeout: time.Second * 5} body := &bytes.Buffer{} writer := multipart.NewWriter(body) fw, err := writer.CreateFormFile("report", "report.html") if err != nil { return err } err = render(fw, tmpl) if err != nil { return err } fw, err = writer.CreateFormField("coverage") if err != nil { return err } if _, err = fmt.Fprintf(fw, "%.03f", coverage); err != nil { return err } fw, err = writer.CreateFormField("secret") if err != nil { return err } if _, err = fw.Write([]byte(os.Getenv("COVERAGE_SECRET"))); err != nil { return err } if err = writer.Close(); err != nil { return err } req, err := http.NewRequestWithContext(context.Background(), "POST", url, body) if err != nil { return err } req.Header.Set("Content-Type", writer.FormDataContentType()) res, err := client.Do(req) if err != nil { return err } resBody, err := io.ReadAll(res.Body) if err != nil { return err } fmt.Printf("Response from coverage server: %s", resBody) if err = res.Body.Close(); err != nil { return err } if res.StatusCode != http.StatusOK { return fmt.Errorf("request did not succeed: (code %d): %s", res.StatusCode, resBody) } return nil }