1
2
3
4
5
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
36
37
38 type Flags []string
39
40
41 func (a *Flags) String() string {
42 return strings.Join(*a, ",")
43 }
44
45
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
80 var usage = func() {
81 cmd := os.Args[0]
82
83 s := "Usage: %s [options]\n"
84 fmt.Fprintf(os.Stderr, s, cmd)
85 flag.PrintDefaults()
86 }
87
88
89
90 type SourceFile struct {
91 Name string `json:"name"`
92 Source string `json:"source"`
93 Coverage []interface{} `json:"coverage"`
94 }
95
96
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
111 type Response struct {
112 Message string `json:"message"`
113 URL string `json:"url"`
114 Error bool `json:"error"`
115 }
116
117
118 type WebHookResponse struct {
119 Done bool `json:"done"`
120 }
121
122
123
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
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
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
235
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
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
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
309
310 if *insecure {
311 http.DefaultTransport.(*http.Transport).TLSClientConfig = &tls.Config{InsecureSkipVerify: true}
312 }
313
314
315
316
317
318
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
353 }
354
355 head := "HEAD"
356 var pullRequest string
357 if prNumber := os.Getenv("CIRCLE_PR_NUMBER"); prNumber != "" {
358
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
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
407
408 if jobID != "" {
409 j.ServiceJobID = jobID
410 }
411 j.ServiceJobNumber = *jobNumber
412
413
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
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