...

Source file src/github.com/mattn/goveralls/goveralls.go

Documentation: github.com/mattn/goveralls

     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  	"crypto/tls"
    12  	"encoding/json"
    13  	"errors"
    14  	"flag"
    15  	"fmt"
    16  	"go/build"
    17  	"io/ioutil"
    18  	"log"
    19  	"net/http"
    20  	"net/url"
    21  	"os"
    22  	"os/exec"
    23  	"path/filepath"
    24  	"regexp"
    25  	"strconv"
    26  	"strings"
    27  	"time"
    28  
    29  	"golang.org/x/tools/cover"
    30  	"golang.org/x/tools/go/buildutil"
    31  )
    32  
    33  /*
    34  	https://coveralls.io/docs/api_reference
    35  */
    36  
    37  // Flags are extra flags to the tests
    38  type Flags []string
    39  
    40  // String implements flag.Value interface.
    41  func (a *Flags) String() string {
    42  	return strings.Join(*a, ",")
    43  }
    44  
    45  // Set implements flag.Value interface.
    46  func (a *Flags) Set(value string) error {
    47  	*a = append(*a, value)
    48  	return nil
    49  }
    50  
    51  var (
    52  	extraFlags  Flags
    53  	pkg         = flag.String("package", "", "Go package")
    54  	verbose     = flag.Bool("v", false, "Pass '-v' argument to 'go test' and output to stdout")
    55  	race        = flag.Bool("race", false, "Pass '-race' argument to 'go test'")
    56  	debug       = flag.Bool("debug", false, "Enable debug output")
    57  	coverprof   = flag.String("coverprofile", "", "If supplied, use a go cover profile (comma separated)")
    58  	covermode   = flag.String("covermode", "count", "sent as covermode argument to go test")
    59  	repotoken   = flag.String("repotoken", os.Getenv("COVERALLS_TOKEN"), "Repository Token on coveralls")
    60  	reponame    = flag.String("reponame", "", "Repository name")
    61  	parallel    = flag.Bool("parallel", os.Getenv("COVERALLS_PARALLEL") != "", "Submit as parallel")
    62  	endpoint    = flag.String("endpoint", "https://coveralls.io", "Hostname to submit Coveralls data to")
    63  	service     = flag.String("service", "", "The CI service or other environment in which the test suite was run. ")
    64  	shallow     = flag.Bool("shallow", false, "Shallow coveralls internal server errors")
    65  	ignore      = flag.String("ignore", "", "Comma separated files to ignore")
    66  	insecure    = flag.Bool("insecure", false, "Set insecure to skip verification of certificates")
    67  	show        = flag.Bool("show", false, "Show which package is being tested")
    68  	customJobID = flag.String("jobid", "", "Custom set job token")
    69  	jobNumber   = flag.String("jobnumber", "", "Custom set job number")
    70  	flagName    = flag.String("flagname", os.Getenv("COVERALLS_FLAG_NAME"), "Job flag name, e.g. \"Unit\", \"Functional\", or \"Integration\". Will be shown in the Coveralls UI.")
    71  
    72  	parallelFinish = flag.Bool("parallel-finish", false, "finish parallel test")
    73  )
    74  
    75  func init() {
    76  	flag.Var((*buildutil.TagsFlag)(&build.Default.BuildTags), "tags", buildutil.TagsFlagDoc)
    77  }
    78  
    79  // usage supplants package flag's Usage variable
    80  var usage = func() {
    81  	cmd := os.Args[0]
    82  	// fmt.Fprintf(os.Stderr, "Usage of %s:\n", cmd)
    83  	s := "Usage: %s [options]\n"
    84  	fmt.Fprintf(os.Stderr, s, cmd)
    85  	flag.PrintDefaults()
    86  }
    87  
    88  // A SourceFile represents a source code file and its coverage data for a
    89  // single job.
    90  type SourceFile struct {
    91  	Name     string        `json:"name"`     // File path of this source file
    92  	Source   string        `json:"source"`   // Full source code of this file
    93  	Coverage []interface{} `json:"coverage"` // Requires both nulls and integers
    94  }
    95  
    96  // A Job represents the coverage data from a single run of a test suite.
    97  type Job struct {
    98  	RepoToken          *string       `json:"repo_token,omitempty"`
    99  	ServiceJobID       string        `json:"service_job_id"`
   100  	ServiceJobNumber   string        `json:"service_job_number,omitempty"`
   101  	ServicePullRequest string        `json:"service_pull_request,omitempty"`
   102  	ServiceName        string        `json:"service_name"`
   103  	FlagName           string        `json:"flag_name,omitempty"`
   104  	SourceFiles        []*SourceFile `json:"source_files"`
   105  	Parallel           *bool         `json:"parallel,omitempty"`
   106  	Git                *Git          `json:"git,omitempty"`
   107  	RunAt              time.Time     `json:"run_at"`
   108  }
   109  
   110  // A Response is returned by the Coveralls.io API.
   111  type Response struct {
   112  	Message string `json:"message"`
   113  	URL     string `json:"url"`
   114  	Error   bool   `json:"error"`
   115  }
   116  
   117  // A WebHookResponse is returned by the Coveralls.io WebHook.
   118  type WebHookResponse struct {
   119  	Done bool `json:"done"`
   120  }
   121  
   122  // getPkgs returns packages for measuring coverage. Returned packages doesn't
   123  // contain vendor packages.
   124  func getPkgs(pkg string) ([]string, error) {
   125  	if pkg == "" {
   126  		pkg = "./..."
   127  	}
   128  	out, err := exec.Command("go", "list", pkg).CombinedOutput()
   129  	if err != nil {
   130  		return nil, err
   131  	}
   132  	allPkgs := strings.Split(strings.Trim(string(out), "\n"), "\n")
   133  	pkgs := make([]string, 0, len(allPkgs))
   134  	for _, p := range allPkgs {
   135  		if strings.Contains(p, "/vendor/") {
   136  			continue
   137  		}
   138  		// go modules output
   139  		if strings.Contains(p, "go: ") {
   140  			continue
   141  		}
   142  		pkgs = append(pkgs, p)
   143  	}
   144  	return pkgs, nil
   145  }
   146  
   147  func getCoverage() ([]*SourceFile, error) {
   148  	if *coverprof != "" {
   149  		return parseCover(*coverprof)
   150  	}
   151  
   152  	// pkgs is packages to run tests and get coverage.
   153  	pkgs, err := getPkgs(*pkg)
   154  	if err != nil {
   155  		return nil, err
   156  	}
   157  	coverpkg := fmt.Sprintf("-coverpkg=%s", strings.Join(pkgs, ","))
   158  	var pfss [][]*cover.Profile
   159  	for _, line := range pkgs {
   160  		f, err := ioutil.TempFile("", "goveralls")
   161  		if err != nil {
   162  			return nil, err
   163  		}
   164  		f.Close()
   165  		cmd := exec.Command("go")
   166  		outBuf := new(bytes.Buffer)
   167  		cmd.Stdout = outBuf
   168  		cmd.Stderr = outBuf
   169  		coverm := *covermode
   170  		if *race {
   171  			coverm = "atomic"
   172  		}
   173  		args := []string{"go", "test", "-covermode", coverm, "-coverprofile", f.Name(), coverpkg}
   174  		if *verbose {
   175  			args = append(args, "-v")
   176  			cmd.Stdout = os.Stdout
   177  		}
   178  		if *race {
   179  			args = append(args, "-race")
   180  		}
   181  		args = append(args, extraFlags...)
   182  		args = append(args, line)
   183  		cmd.Args = args
   184  
   185  		if *show {
   186  			fmt.Println("goveralls:", line)
   187  		}
   188  		err = cmd.Run()
   189  		if err != nil {
   190  			return nil, fmt.Errorf("%v: %v", err, outBuf.String())
   191  		}
   192  
   193  		pfs, err := cover.ParseProfiles(f.Name())
   194  		if err != nil {
   195  			return nil, err
   196  		}
   197  		err = os.Remove(f.Name())
   198  		if err != nil {
   199  			return nil, err
   200  		}
   201  		pfss = append(pfss, pfs)
   202  	}
   203  
   204  	sourceFiles, err := toSF(mergeProfs(pfss))
   205  	if err != nil {
   206  		return nil, err
   207  	}
   208  
   209  	return sourceFiles, nil
   210  }
   211  
   212  var vscDirs = []string{".git", ".hg", ".bzr", ".svn"}
   213  
   214  func findRepositoryRoot(dir string) (string, bool) {
   215  	for _, vcsdir := range vscDirs {
   216  		if d, err := os.Stat(filepath.Join(dir, vcsdir)); err == nil && d.IsDir() {
   217  			return dir, true
   218  		}
   219  	}
   220  	nextdir := filepath.Dir(dir)
   221  	if nextdir == dir {
   222  		return "", false
   223  	}
   224  	return findRepositoryRoot(nextdir)
   225  }
   226  
   227  func getCoverallsSourceFileName(name string) string {
   228  	if dir, ok := findRepositoryRoot(name); ok {
   229  		name = strings.TrimPrefix(name, dir+string(os.PathSeparator))
   230  	}
   231  	return filepath.ToSlash(name)
   232  }
   233  
   234  // processParallelFinish notifies coveralls that all jobs are completed
   235  // ref. https://docs.coveralls.io/parallel-build-webhook
   236  func processParallelFinish(jobID, token string) error {
   237  	var name string
   238  	if reponame != nil && *reponame != "" {
   239  		name = *reponame
   240  	} else if s := os.Getenv("GITHUB_REPOSITORY"); s != "" {
   241  		name = s
   242  	}
   243  
   244  	params := make(url.Values)
   245  	params.Set("repo_token", token)
   246  	params.Set("repo_name", name)
   247  	params.Set("payload[build_num]", jobID)
   248  	params.Set("payload[status]", "done")
   249  	res, err := http.PostForm(*endpoint+"/webhook", params)
   250  	if err != nil {
   251  		return err
   252  	}
   253  	defer res.Body.Close()
   254  	bodyBytes, err := ioutil.ReadAll(res.Body)
   255  	if err != nil {
   256  		return fmt.Errorf("Unable to read response body from coveralls: %s", err)
   257  	}
   258  
   259  	if res.StatusCode >= http.StatusInternalServerError && *shallow {
   260  		fmt.Println("coveralls server failed internally")
   261  		return nil
   262  	}
   263  
   264  	if res.StatusCode != 200 {
   265  		return fmt.Errorf("Bad response status from coveralls: %d\n%s", res.StatusCode, bodyBytes)
   266  	}
   267  
   268  	var response WebHookResponse
   269  	if err = json.Unmarshal(bodyBytes, &response); err != nil {
   270  		return fmt.Errorf("Unable to unmarshal response JSON from coveralls: %s\n%s", err, bodyBytes)
   271  	}
   272  
   273  	if !response.Done {
   274  		return fmt.Errorf("jobs are not completed:\n%s", bodyBytes)
   275  	}
   276  
   277  	return nil
   278  }
   279  
   280  func process() error {
   281  	log.SetFlags(log.Ltime | log.Lshortfile)
   282  	//
   283  	// Parse Flags
   284  	//
   285  	flag.Usage = usage
   286  	flag.Var(&extraFlags, "flags", "extra flags to the tests")
   287  	flag.Parse()
   288  	if len(flag.Args()) > 0 {
   289  		flag.Usage()
   290  		os.Exit(1)
   291  	}
   292  
   293  	//
   294  	// Setup PATH environment variable
   295  	//
   296  	paths := filepath.SplitList(os.Getenv("PATH"))
   297  	if goroot := os.Getenv("GOROOT"); goroot != "" {
   298  		paths = append(paths, filepath.Join(goroot, "bin"))
   299  	}
   300  	if gopath := os.Getenv("GOPATH"); gopath != "" {
   301  		for _, path := range filepath.SplitList(gopath) {
   302  			paths = append(paths, filepath.Join(path, "bin"))
   303  		}
   304  	}
   305  	os.Setenv("PATH", strings.Join(paths, string(filepath.ListSeparator)))
   306  
   307  	//
   308  	// Handle certificate verification configuration
   309  	//
   310  	if *insecure {
   311  		http.DefaultTransport.(*http.Transport).TLSClientConfig = &tls.Config{InsecureSkipVerify: true}
   312  	}
   313  
   314  	//
   315  	// Initialize Job
   316  	//
   317  
   318  	// flags are never nil, so no nil check needed
   319  	githubEvent := getGithubEvent()
   320  	var jobID string
   321  	if *customJobID != "" {
   322  		jobID = *customJobID
   323  	} else if ServiceJobID := os.Getenv("COVERALLS_SERVICE_JOB_ID"); ServiceJobID != "" {
   324  		jobID = ServiceJobID
   325  	} else if travisjobID := os.Getenv("TRAVIS_JOB_ID"); travisjobID != "" {
   326  		jobID = travisjobID
   327  	} else if circleCIJobID := os.Getenv("CIRCLE_BUILD_NUM"); circleCIJobID != "" {
   328  		jobID = circleCIJobID
   329  	} else if appveyorJobID := os.Getenv("APPVEYOR_JOB_ID"); appveyorJobID != "" {
   330  		jobID = appveyorJobID
   331  	} else if semaphorejobID := os.Getenv("SEMAPHORE_BUILD_NUMBER"); semaphorejobID != "" {
   332  		jobID = semaphorejobID
   333  	} else if jenkinsjobID := os.Getenv("BUILD_NUMBER"); jenkinsjobID != "" {
   334  		jobID = jenkinsjobID
   335  	} else if buildID := os.Getenv("BUILDKITE_BUILD_ID"); buildID != "" {
   336  		jobID = buildID
   337  	} else if droneBuildNumber := os.Getenv("DRONE_BUILD_NUMBER"); droneBuildNumber != "" {
   338  		jobID = droneBuildNumber
   339  	} else if buildkiteBuildNumber := os.Getenv("BUILDKITE_BUILD_NUMBER"); buildkiteBuildNumber != "" {
   340  		jobID = buildkiteBuildNumber
   341  	} else if codeshipjobID := os.Getenv("CI_BUILD_ID"); codeshipjobID != "" {
   342  		jobID = codeshipjobID
   343  	} else if githubRunID := os.Getenv("GITHUB_RUN_ID"); githubRunID != "" {
   344  		jobID = githubRunID
   345  	}
   346  
   347  	if *parallelFinish {
   348  		return processParallelFinish(jobID, *repotoken)
   349  	}
   350  
   351  	if *repotoken == "" {
   352  		repotoken = nil // remove the entry from json
   353  	}
   354  
   355  	head := "HEAD"
   356  	var pullRequest string
   357  	if prNumber := os.Getenv("CIRCLE_PR_NUMBER"); prNumber != "" {
   358  		// for Circle CI (pull request from forked repo)
   359  		pullRequest = prNumber
   360  	} else if prNumber := os.Getenv("TRAVIS_PULL_REQUEST"); prNumber != "" && prNumber != "false" {
   361  		pullRequest = prNumber
   362  	} else if prURL := os.Getenv("CI_PULL_REQUEST"); prURL != "" {
   363  		// for Circle CI
   364  		pullRequest = regexp.MustCompile(`[0-9]+$`).FindString(prURL)
   365  	} else if prNumber := os.Getenv("APPVEYOR_PULL_REQUEST_NUMBER"); prNumber != "" {
   366  		pullRequest = prNumber
   367  	} else if prNumber := os.Getenv("PULL_REQUEST_NUMBER"); prNumber != "" {
   368  		pullRequest = prNumber
   369  	} else if prNumber := os.Getenv("BUILDKITE_PULL_REQUEST"); prNumber != "" {
   370  		pullRequest = prNumber
   371  	} else if prNumber := os.Getenv("DRONE_PULL_REQUEST"); prNumber != "" {
   372  		pullRequest = prNumber
   373  	} else if prNumber := os.Getenv("BUILDKITE_PULL_REQUEST"); prNumber != "" {
   374  		pullRequest = prNumber
   375  	} else if prNumber := os.Getenv("CI_PR_NUMBER"); prNumber != "" {
   376  		pullRequest = prNumber
   377  	} else if os.Getenv("GITHUB_EVENT_NAME") == "pull_request" {
   378  		number := githubEvent["number"].(float64)
   379  		pullRequest = strconv.Itoa(int(number))
   380  
   381  		ghPR := githubEvent["pull_request"].(map[string]interface{})
   382  		ghHead := ghPR["head"].(map[string]interface{})
   383  		head = ghHead["sha"].(string)
   384  	}
   385  
   386  	if *service == "" && os.Getenv("TRAVIS_JOB_ID") != "" {
   387  		*service = "travis-ci"
   388  	}
   389  
   390  	sourceFiles, err := getCoverage()
   391  	if err != nil {
   392  		return err
   393  	}
   394  
   395  	j := Job{
   396  		RunAt:              time.Now(),
   397  		RepoToken:          repotoken,
   398  		ServicePullRequest: pullRequest,
   399  		Parallel:           parallel,
   400  		Git:                collectGitInfo(head),
   401  		SourceFiles:        sourceFiles,
   402  		ServiceName:        *service,
   403  		FlagName:           *flagName,
   404  	}
   405  
   406  	// Only include a job ID if it's known, otherwise, Coveralls looks
   407  	// for the job and can't find it.
   408  	if jobID != "" {
   409  		j.ServiceJobID = jobID
   410  	}
   411  	j.ServiceJobNumber = *jobNumber
   412  
   413  	// Ignore files
   414  	if len(*ignore) > 0 {
   415  		patterns := strings.Split(*ignore, ",")
   416  		for i, pattern := range patterns {
   417  			patterns[i] = strings.TrimSpace(pattern)
   418  		}
   419  		var files []*SourceFile
   420  	Files:
   421  		for _, file := range j.SourceFiles {
   422  			for _, pattern := range patterns {
   423  				match, err := filepath.Match(pattern, file.Name)
   424  				if err != nil {
   425  					return err
   426  				}
   427  				if match {
   428  					fmt.Printf("ignoring %s\n", file.Name)
   429  					continue Files
   430  				}
   431  			}
   432  			files = append(files, file)
   433  		}
   434  		j.SourceFiles = files
   435  	}
   436  
   437  	if *debug {
   438  		b, err := json.MarshalIndent(j, "", "  ")
   439  		if err != nil {
   440  			return err
   441  		}
   442  		log.Printf("Posting data: %s", b)
   443  	}
   444  
   445  	b, err := json.Marshal(j)
   446  	if err != nil {
   447  		return err
   448  	}
   449  
   450  	params := make(url.Values)
   451  	params.Set("json", string(b))
   452  	res, err := http.PostForm(*endpoint+"/api/v1/jobs", params)
   453  	if err != nil {
   454  		return err
   455  	}
   456  	defer res.Body.Close()
   457  	bodyBytes, err := ioutil.ReadAll(res.Body)
   458  	if err != nil {
   459  		return fmt.Errorf("Unable to read response body from coveralls: %s", err)
   460  	}
   461  
   462  	if res.StatusCode >= http.StatusInternalServerError && *shallow {
   463  		fmt.Println("coveralls server failed internally")
   464  		return nil
   465  	}
   466  
   467  	if res.StatusCode != 200 {
   468  		return fmt.Errorf("Bad response status from coveralls: %d\n%s", res.StatusCode, bodyBytes)
   469  	}
   470  	var response Response
   471  	if err = json.Unmarshal(bodyBytes, &response); err != nil {
   472  		return fmt.Errorf("Unable to unmarshal response JSON from coveralls: %s\n%s", err, bodyBytes)
   473  	}
   474  	if response.Error {
   475  		return errors.New(response.Message)
   476  	}
   477  	fmt.Println(response.Message)
   478  	fmt.Println(response.URL)
   479  	return nil
   480  }
   481  
   482  func getGithubEvent() map[string]interface{} {
   483  	jsonFilePath := os.Getenv("GITHUB_EVENT_PATH")
   484  	if jsonFilePath == "" {
   485  		return nil
   486  	}
   487  
   488  	jsonFile, err := os.Open(jsonFilePath)
   489  	if err != nil {
   490  		log.Fatal(err)
   491  	}
   492  	defer jsonFile.Close()
   493  
   494  	jsonByte, _ := ioutil.ReadAll(jsonFile)
   495  
   496  	// unmarshal the json into a release event
   497  	var event map[string]interface{}
   498  	err = json.Unmarshal(jsonByte, &event)
   499  	if err != nil {
   500  		log.Fatal(err)
   501  	}
   502  
   503  	return event
   504  }
   505  
   506  func main() {
   507  	if err := process(); err != nil {
   508  		fmt.Fprintf(os.Stderr, "%s\n", err)
   509  		os.Exit(1)
   510  	}
   511  }
   512  

View as plain text