| | | 1 | | package reporter |
| | | 2 | | |
| | | 3 | | import ( |
| | | 4 | | "context" |
| | | 5 | | "encoding/json" |
| | | 6 | | "fmt" |
| | | 7 | | "io" |
| | | 8 | | "sort" |
| | | 9 | | |
| | | 10 | | "github.com/jedi-knights/neospec/internal/domain" |
| | | 11 | | ) |
| | | 12 | | |
| | | 13 | | // Coveralls writes coverage data in the Coveralls JSON API format. |
| | | 14 | | // https://docs.coveralls.io/api-reference |
| | | 15 | | type Coveralls struct{} |
| | | 16 | | |
| | | 17 | | // NewCoveralls creates a Coveralls reporter. |
| | | 18 | | func NewCoveralls() *Coveralls { return &Coveralls{} } |
| | | 19 | | |
| | | 20 | | // coverallsPayload is the top-level Coveralls JSON structure. |
| | | 21 | | type coverallsPayload struct { |
| | | 22 | | RepoToken string `json:"repo_token,omitempty"` |
| | | 23 | | ServiceName string `json:"service_name"` |
| | | 24 | | SourceFiles []coverallsSource `json:"source_files"` |
| | | 25 | | } |
| | | 26 | | |
| | | 27 | | // coverallsSource represents a single source file in the Coveralls format. |
| | | 28 | | // Coverage is a sparse array where index is line number - 1, value is hit count |
| | | 29 | | // or null for non-executable lines. |
| | | 30 | | type coverallsSource struct { |
| | | 31 | | Name string `json:"name"` |
| | | 32 | | Coverage []*int `json:"coverage"` // nil = not executable |
| | | 33 | | } |
| | | 34 | | |
| | 4 | 35 | | func (c *Coveralls) Write(_ context.Context, w io.Writer, _ *domain.SuiteResult, cov *domain.CoverageData) error { |
| | 2 | 36 | | if cov == nil { |
| | 2 | 37 | | cov = &domain.CoverageData{} |
| | 2 | 38 | | } |
| | | 39 | | |
| | 4 | 40 | | payload := coverallsPayload{ |
| | 4 | 41 | | ServiceName: "neospec", |
| | 4 | 42 | | } |
| | 4 | 43 | | |
| | 3 | 44 | | for _, file := range cov.Files { |
| | 1 | 45 | | if len(file.Lines) == 0 { |
| | 1 | 46 | | continue |
| | | 47 | | } |
| | | 48 | | |
| | | 49 | | // Find the maximum line number to size the coverage array. |
| | 2 | 50 | | maxLine := 0 |
| | 2 | 51 | | lines := make([]int, 0, len(file.Lines)) |
| | 2 | 52 | | for ln := range file.Lines { |
| | 4 | 53 | | lines = append(lines, ln) |
| | 4 | 54 | | if ln > maxLine { |
| | 4 | 55 | | maxLine = ln |
| | 4 | 56 | | } |
| | | 57 | | } |
| | 2 | 58 | | sort.Ints(lines) |
| | 2 | 59 | | |
| | 2 | 60 | | // Coveralls coverage array is 0-indexed (line N is at index N-1). |
| | 2 | 61 | | coverage := make([]*int, maxLine) |
| | 2 | 62 | | for _, ln := range lines { |
| | 4 | 63 | | hits := file.Lines[ln] |
| | 4 | 64 | | coverage[ln-1] = &hits |
| | 4 | 65 | | } |
| | | 66 | | |
| | 2 | 67 | | payload.SourceFiles = append(payload.SourceFiles, coverallsSource{ |
| | 2 | 68 | | Name: file.Path, |
| | 2 | 69 | | Coverage: coverage, |
| | 2 | 70 | | }) |
| | | 71 | | } |
| | | 72 | | |
| | 4 | 73 | | data, err := json.MarshalIndent(payload, "", " ") |
| | 0 | 74 | | if err != nil { |
| | 0 | 75 | | return fmt.Errorf("marshaling coveralls JSON: %w", err) |
| | 0 | 76 | | } |
| | 4 | 77 | | _, err = fmt.Fprintln(w, string(data)) |
| | 4 | 78 | | return err |
| | | 79 | | } |