github.com/vlal/goveralls@v0.0.2-0.20171114042957-b71a1e4855f8/goveralls.go (about)

     1  // Copyright (c) 2013 Yasuhiro Matsumoto, Jason McVetta.
     2  // This is Free Software,  released under the MIT license.
     3  // See http://mattn.mit-license.org/2013 for details.
     4  
     5  // goveralls is a Go client for Coveralls.io.
     6  package main
     7  
     8  import (
     9  	"bytes"
    10  	_ "crypto/sha512"
    11  	"encoding/json"
    12  	"errors"
    13  	"flag"
    14  	"fmt"
    15  	"io/ioutil"
    16  	"log"
    17  	"net/http"
    18  	"net/url"
    19  	"os"
    20  	"os/exec"
    21  	"path/filepath"
    22  	"regexp"
    23  	"strings"
    24  	"time"
    25  
    26  	"golang.org/x/tools/cover"
    27  )
    28  
    29  /*
    30  	https://coveralls.io/docs/api_reference
    31  */
    32  
    33  type Flags []string
    34  
    35  func (a *Flags) String() string {
    36  	return strings.Join(*a, ",")
    37  }
    38  
    39  func (a *Flags) Set(value string) error {
    40  	*a = append(*a, value)
    41  	return nil
    42  }
    43  
    44  var (
    45  	extraFlags Flags
    46  	pkg        = flag.String("package", "", "Go package")
    47  	verbose    = flag.Bool("v", false, "Pass '-v' argument to 'go test' and output to stdout")
    48  	race       = flag.Bool("race", false, "Pass '-race' argument to 'go test'")
    49  	debug      = flag.Bool("debug", false, "Enable debug output")
    50  	coverprof  = flag.String("coverprofile", "", "If supplied, use a go cover profile (comma separated)")
    51  	covermode  = flag.String("covermode", "count", "sent as covermode argument to go test")
    52  	repotoken  = flag.String("repotoken", os.Getenv("COVERALLS_TOKEN"), "Repository Token on coveralls")
    53  	endpoint   = flag.String("endpoint", "https://coveralls.io", "Hostname to submit Coveralls data to")
    54  	service    = flag.String("service", "travis-ci", "The CI service or other environment in which the test suite was run. ")
    55  	shallow    = flag.Bool("shallow", false, "Shallow coveralls internal server errors")
    56  	ignore     = flag.String("ignore", "", "Comma separated files to ignore")
    57  )
    58  
    59  // usage supplants package flag's Usage variable
    60  var usage = func() {
    61  	cmd := os.Args[0]
    62  	// fmt.Fprintf(os.Stderr, "Usage of %s:\n", cmd)
    63  	s := "Usage: %s [options]\n"
    64  	fmt.Fprintf(os.Stderr, s, cmd)
    65  	flag.PrintDefaults()
    66  }
    67  
    68  // A SourceFile represents a source code file and its coverage data for a
    69  // single job.
    70  type SourceFile struct {
    71  	Name     string        `json:"name"`     // File path of this source file
    72  	Source   string        `json:"source"`   // Full source code of this file
    73  	Coverage []interface{} `json:"coverage"` // Requires both nulls and integers
    74  }
    75  
    76  // A Job represents the coverage data from a single run of a test suite.
    77  type Job struct {
    78  	RepoToken          *string       `json:"repo_token,omitempty"`
    79  	ServiceJobId       string        `json:"service_job_id"`
    80  	ServicePullRequest string        `json:"service_pull_request,omitempty"`
    81  	ServiceName        string        `json:"service_name"`
    82  	SourceFiles        []*SourceFile `json:"source_files"`
    83  	Git                *Git          `json:"git,omitempty"`
    84  	RunAt              time.Time     `json:"run_at"`
    85  }
    86  
    87  // A Response is returned by the Coveralls.io API.
    88  type Response struct {
    89  	Message string `json:"message"`
    90  	URL     string `json:"url"`
    91  	Error   bool   `json:"error"`
    92  }
    93  
    94  // getPkgs returns packages for mesuring coverage. Returned packages doesn't
    95  // contain vendor packages.
    96  func getPkgs(pkg string) ([]string, error) {
    97  	if pkg == "" {
    98  		pkg = "./..."
    99  	}
   100  	out, err := exec.Command("go", "list", pkg).CombinedOutput()
   101  	if err != nil {
   102  		return nil, err
   103  	}
   104  	allPkgs := strings.Split(strings.Trim(string(out), "\n"), "\n")
   105  	pkgs := make([]string, 0, len(allPkgs))
   106  	for _, p := range allPkgs {
   107  		if !strings.Contains(p, "/vendor/") {
   108  			pkgs = append(pkgs, p)
   109  		}
   110  	}
   111  	return pkgs, nil
   112  }
   113  
   114  func getCoverage() ([]*SourceFile, error) {
   115  	if *coverprof != "" {
   116  		return parseCover(*coverprof)
   117  	}
   118  
   119  	// pkgs is packages to run tests and get coverage.
   120  	pkgs, err := getPkgs(*pkg)
   121  	if err != nil {
   122  		return nil, err
   123  	}
   124  	coverpkg := fmt.Sprintf("-coverpkg=%s", strings.Join(pkgs, ","))
   125  	var pfss [][]*cover.Profile
   126  	for _, line := range pkgs {
   127  		f, err := ioutil.TempFile("", "goveralls")
   128  		if err != nil {
   129  			return nil, err
   130  		}
   131  		f.Close()
   132  		cmd := exec.Command("go")
   133  		outBuf := new(bytes.Buffer)
   134  		cmd.Stdout = outBuf
   135  		cmd.Stderr = outBuf
   136  		coverm := *covermode
   137  		if *race {
   138  			coverm = "atomic"
   139  		}
   140  		args := []string{"go", "test", "-covermode", coverm, "-coverprofile", f.Name(), coverpkg}
   141  		if *verbose {
   142  			args = append(args, "-v")
   143  			cmd.Stdout = os.Stdout
   144  		}
   145  		if *race {
   146  			args = append(args, "-race")
   147  		}
   148  		args = append(args, extraFlags...)
   149  		args = append(args, line)
   150  		cmd.Args = args
   151  
   152  		err = cmd.Run()
   153  		if err != nil {
   154  			return nil, fmt.Errorf("%v: %v", err, outBuf.String())
   155  		}
   156  
   157  		pfs, err := cover.ParseProfiles(f.Name())
   158  		if err != nil {
   159  			return nil, err
   160  		}
   161  		err = os.Remove(f.Name())
   162  		if err != nil {
   163  			return nil, err
   164  		}
   165  		pfss = append(pfss, pfs)
   166  	}
   167  
   168  	sourceFiles, err := toSF(mergeProfs(pfss))
   169  	if err != nil {
   170  		return nil, err
   171  	}
   172  
   173  	return sourceFiles, nil
   174  }
   175  
   176  var vscDirs = []string{".git", ".hg", ".bzr", ".svn"}
   177  
   178  func findRepositoryRoot(dir string) (string, bool) {
   179  	for _, vcsdir := range vscDirs {
   180  		if d, err := os.Stat(filepath.Join(dir, vcsdir)); err == nil && d.IsDir() {
   181  			return dir, true
   182  		}
   183  	}
   184  	nextdir := filepath.Dir(dir)
   185  	if nextdir == dir {
   186  		return "", false
   187  	}
   188  	return findRepositoryRoot(nextdir)
   189  }
   190  
   191  func getCoverallsSourceFileName(name string) string {
   192  	if dir, ok := findRepositoryRoot(name); !ok {
   193  		return name
   194  	} else {
   195  		filename := strings.TrimPrefix(name, dir+string(os.PathSeparator))
   196  		return filename
   197  	}
   198  }
   199  
   200  func process() error {
   201  	log.SetFlags(log.Ltime | log.Lshortfile)
   202  	//
   203  	// Parse Flags
   204  	//
   205  	flag.Usage = usage
   206  	flag.Var(&extraFlags, "flags", "extra flags to the tests")
   207  	flag.Parse()
   208  	if len(flag.Args()) > 0 {
   209  		flag.Usage()
   210  		os.Exit(1)
   211  	}
   212  
   213  	//
   214  	// Setup PATH environment variable
   215  	//
   216  	paths := filepath.SplitList(os.Getenv("PATH"))
   217  	if goroot := os.Getenv("GOROOT"); goroot != "" {
   218  		paths = append(paths, filepath.Join(goroot, "bin"))
   219  	}
   220  	if gopath := os.Getenv("GOPATH"); gopath != "" {
   221  		for _, path := range filepath.SplitList(gopath) {
   222  			paths = append(paths, filepath.Join(path, "bin"))
   223  		}
   224  	}
   225  	os.Setenv("PATH", strings.Join(paths, string(filepath.ListSeparator)))
   226  
   227  	//
   228  	// Initialize Job
   229  	//
   230  	var jobId string
   231  	if travisJobId := os.Getenv("TRAVIS_JOB_ID"); travisJobId != "" {
   232  		jobId = travisJobId
   233  	} else if circleCiJobId := os.Getenv("CIRCLE_BUILD_NUM"); circleCiJobId != "" {
   234  		jobId = circleCiJobId
   235  	} else if appveyorJobId := os.Getenv("APPVEYOR_JOB_ID"); appveyorJobId != "" {
   236  		jobId = appveyorJobId
   237  	}
   238  
   239  	if *repotoken == "" {
   240  		repotoken = nil // remove the entry from json
   241  	}
   242  	var pullRequest string
   243  	if prNumber := os.Getenv("CIRCLE_PR_NUMBER"); prNumber != "" {
   244  		// for Circle CI (pull request from forked repo)
   245  		pullRequest = prNumber
   246  	} else if prNumber := os.Getenv("TRAVIS_PULL_REQUEST"); prNumber != "" && prNumber != "false" {
   247  		pullRequest = prNumber
   248  	} else if prURL := os.Getenv("CI_PULL_REQUEST"); prURL != "" {
   249  		// for Circle CI
   250  		pullRequest = regexp.MustCompile(`[0-9]+$`).FindString(prURL)
   251  	} else if prNumber := os.Getenv("APPVEYOR_PULL_REQUEST_NUMBER"); prNumber != "" {
   252  		pullRequest = prNumber
   253  	}
   254  
   255  	sourceFiles, err := getCoverage()
   256  	if err != nil {
   257  		return err
   258  	}
   259  
   260  	j := Job{
   261  		RunAt:              time.Now(),
   262  		RepoToken:          repotoken,
   263  		ServicePullRequest: pullRequest,
   264  		Git:                collectGitInfo(),
   265  		SourceFiles:        sourceFiles,
   266  	}
   267  
   268  	// Only include a job ID if it's known, otherwise, Coveralls looks
   269  	// for the job and can't find it.
   270  	if jobId != "" {
   271  		j.ServiceJobId = jobId
   272  		j.ServiceName = *service
   273  	}
   274  
   275  	// Ignore files
   276  	if len(*ignore) > 0 {
   277  		patterns := strings.Split(*ignore, ",")
   278  		for i, pattern := range patterns {
   279  			patterns[i] = strings.TrimSpace(pattern)
   280  		}
   281  		var files []*SourceFile
   282  	Files:
   283  		for _, file := range j.SourceFiles {
   284  			for _, pattern := range patterns {
   285  				match, err := filepath.Match(pattern, file.Name)
   286  				if err != nil {
   287  					return err
   288  				}
   289  				if match {
   290  					fmt.Printf("ignoring %s\n", file.Name)
   291  					continue Files
   292  				}
   293  			}
   294  			files = append(files, file)
   295  		}
   296  		j.SourceFiles = files
   297  	}
   298  
   299  	if *debug {
   300  		b, err := json.MarshalIndent(j, "", "  ")
   301  		if err != nil {
   302  			return err
   303  		}
   304  		log.Printf("Posting data: %s", b)
   305  	}
   306  
   307  	b, err := json.Marshal(j)
   308  	if err != nil {
   309  		return err
   310  	}
   311  
   312  	params := make(url.Values)
   313  	params.Set("json", string(b))
   314  	res, err := http.PostForm(*endpoint+"/api/v1/jobs", params)
   315  	if err != nil {
   316  		return err
   317  	}
   318  	defer res.Body.Close()
   319  	bodyBytes, err := ioutil.ReadAll(res.Body)
   320  	if err != nil {
   321  		return fmt.Errorf("Unable to read response body from coveralls: %s", err)
   322  	}
   323  
   324  	if res.StatusCode >= http.StatusInternalServerError && *shallow {
   325  		fmt.Println("coveralls server failed internally")
   326  		return nil
   327  	}
   328  
   329  	if res.StatusCode != 200 {
   330  		return fmt.Errorf("Bad response status from coveralls: %d\n%s", res.StatusCode, bodyBytes)
   331  	}
   332  	var response Response
   333  	if err = json.Unmarshal(bodyBytes, &response); err != nil {
   334  		return fmt.Errorf("Unable to unmarshal response JSON from coveralls: %s\n%s", err, bodyBytes)
   335  	}
   336  	if response.Error {
   337  		return errors.New(response.Message)
   338  	}
   339  	fmt.Println(response.Message)
   340  	fmt.Println(response.URL)
   341  	return nil
   342  }
   343  
   344  func main() {
   345  	if err := process(); err != nil {
   346  		fmt.Fprintf(os.Stderr, "%s\n", err)
   347  		os.Exit(1)
   348  	}
   349  }