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 }