coverage/cmd/ci/main.go

177 lines
4.1 KiB
Go

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
}