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