codeclimate/test-reporter

View on GitHub
upload/uploader.go

Summary

Maintainability
A
1 hr
Test Coverage
package upload

import (
    "bytes"
    "crypto/tls"
    "crypto/x509"
    "encoding/json"
    "fmt"
    "io"
    "io/ioutil"
    "log"
    "net/http"
    "net/url"
    "os"
    "strings"
    "time"

    "github.com/Sirupsen/logrus"
    "github.com/codeclimate/test-reporter/formatters"
    "github.com/codeclimate/test-reporter/version"
    "github.com/pkg/errors"
)

type Uploader struct {
    ReporterID  string
    EndpointURL string
    BatchSize   int
    Input       io.Reader
    Insecure    bool
}

type ErrConflict struct {
    message string
}

func (e *ErrConflict) Error() string {
    return e.message
}

func (u Uploader) Upload() error {
    if u.ReporterID == "" {
        return errors.New("you must supply a CC_TEST_REPORTER_ID ENV variable or pass it via the -r flag")
    }

    rep := formatters.Report{
        SourceFiles: formatters.SourceFiles{},
    }

    err := json.NewDecoder(u.Input).Decode(&rep)
    if err != nil {
        return errors.WithStack(err)
    }

    testReport := NewTestReport(rep)

    pr, pw := io.Pipe()
    go func() {
        defer pw.Close()
        bb := &bytes.Buffer{}
        w := io.MultiWriter(pw, bb)
        err := json.NewEncoder(w).Encode(JSONWraper{Data: testReport})
        if err != nil {
            logrus.Error(err)
            return
        }
        logrus.Debug(bb.String())
    }()

    res, err := u.doRequest(pr, u.EndpointURL)
    if err != nil {
        switch err.(type) {
        case *ErrConflict:
            logrus.Warnf("%s, skipping upload", err.Error())
            return nil
        default:
            return errors.WithStack(err)
        }
    }

    batchLinks := struct {
        Links struct {
            PostBatch string `json:"post_batch"`
        } `json:"links"`
    }{}

    err = json.NewDecoder(res.Body).Decode(&batchLinks)
    if err != nil {
        return errors.WithStack(err)
    }

    postBatchURL, err := u.TransformPostBatchURL(batchLinks.Links.PostBatch)
    if err != nil {
        return errors.WithStack(err)
    }

    return u.SendBatches(testReport, postBatchURL)
}

func (u Uploader) TransformPostBatchURL(rawURL string) (string, error) {
    parsed, err := url.Parse(rawURL)
    if err != nil {
        return "", err
    }

    if u.Insecure {
        parsed.Scheme = "http"
    } else {
        parsed.Scheme = "https"
    }

    return parsed.String(), nil
}

func (u Uploader) SendBatches(rep *TestReport, url string) error {
    batch := [][]SourceFile{}

    pos := 0
    count := len(rep.SourceFiles) / u.BatchSize
    remainder := len(rep.SourceFiles) % u.BatchSize
    for i := 0; i < count; i++ {
        end := pos + u.BatchSize
        batch = append(batch, rep.SourceFiles[pos:end])
        pos = end
    }
    if remainder > 0 {
        batch = append(batch, rep.SourceFiles[pos:])
    }

    for i, b := range batch {
        pr, pw := io.Pipe()
        go func() {
            defer pw.Close()
            bb := &bytes.Buffer{}
            w := io.MultiWriter(pw, bb)
            err := json.NewEncoder(w).Encode(JSONWraper{
                Data: b,
                Meta: map[string]int{
                    "current": i + 1,
                    "total":   len(batch),
                },
            })
            if err != nil {
                logrus.Error(err)
                return
            }
            logrus.Debug(bb.String())
        }()
        _, err := u.doRequest(pr, url)
        if err != nil {
            return errors.WithStack(err)
        }
    }
    fmt.Println("Test report uploaded successfully to Code Climate")
    return nil
}

func (u Uploader) doRequest(in io.Reader, url string) (*http.Response, error) {
    c := http.Client{
        Transport: u.newTransport(),
        Timeout:   30 * time.Second,
    }

    req, err := u.newRequest(in, url)
    if err != nil {
        return nil, errors.WithStack(err)
    }

    logrus.Debugf("posting request to %s", url)
    res, err := c.Do(req)
    if err != nil {
        return res, errors.WithStack(err)
    }

    if res.StatusCode < 200 || res.StatusCode >= 300 {
        httpBody, err := ioutil.ReadAll(res.Body)
        if err != nil {
            return nil, errors.WithStack(err)
        }

        logrus.Debug(string(httpBody))
        errorMessage, err := getErrorMessage(httpBody)
        if err != nil {
            return nil, errors.WithStack(err)
        }

        if res.StatusCode == 409 {
            return nil, &ErrConflict{
                message: fmt.Sprintf("Conflict when uploading: %s", errorMessage),
            }
        }

        return res, fmt.Errorf("response from %s.\nHTTP %d: %s", url, res.StatusCode, errorMessage)
    }
    return res, nil
}

func (u Uploader) newTransport() (tr http.RoundTripper) {
    sslCertFile := os.Getenv("SSL_CERT_FILE")
    if sslCertFile == "" {
        return tr
    }

    caCert, err := ioutil.ReadFile(sslCertFile)
    if err != nil {
        log.Fatal(err)
    }
    caCertPool := x509.NewCertPool()
    caCertPool.AppendCertsFromPEM(caCert)
    return &http.Transport{
        TLSClientConfig: &tls.Config{
            RootCAs: caCertPool,
        },
    }
}

func (u Uploader) newRequest(in io.Reader, url string) (*http.Request, error) {
    req, err := http.NewRequest("POST", url, in)
    if err != nil {
        return nil, errors.WithStack(err)
    }
    req.Header.Set("User-Agent", fmt.Sprintf("TestReporter/%s (Code Climate, Inc.)", version.Version))
    req.Header.Set("Content-Type", "application/json")
    req.Header.Set("X-CC-Test-Reporter-Id", u.ReporterID)
    req.Header.Set("Accept", "application/vnd.api+json")
    return req, err
}

type apiError struct {
    Detail string `json:"detail"`
}

type errorsResponse struct {
    Errors []apiError `json:"errors"`
}

func getErrorMessage(body []byte) (string, error) {
    var response = new(errorsResponse)
    err := json.Unmarshal(body, &response)
    if err != nil {
        return "", errors.WithStack(err)
    }

    var details []string

    for i := range response.Errors {
        details = append(details, response.Errors[i].Detail)
    }

    return strings.Join(details, ", "), err
}