< Summary - Neospec Coverage

Line coverage
100%
Covered lines: 17
Uncovered lines: 0
Coverable lines: 17
Total lines: 453
Line coverage: 100%
Branch coverage
N/A
Covered branches: 0
Total branches: 0
Branch coverage: N/A
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
File 1: NewCobertura0%00100%
File 2: NewConsole0%00100%
File 2: colorForPct0%00100%
File 3: NewCoveralls0%00100%
File 4: NewJUnit0%00100%
File 5: NewLCOV0%00100%

File(s)

/home/runner/work/neospec/neospec/internal/adapters/reporter/cobertura.go

#LineLine coverage
 1package reporter
 2
 3import (
 4  "context"
 5  "encoding/xml"
 6  "fmt"
 7  "io"
 8  "sort"
 9  "time"
 10
 11  "github.com/jedi-knights/neospec/internal/domain"
 12)
 13
 14// Cobertura writes coverage data in Cobertura XML format.
 15// https://cobertura.github.io/cobertura/
 16type Cobertura struct{}
 17
 18// NewCobertura creates a Cobertura reporter.
 419func NewCobertura() *Cobertura { return &Cobertura{} }
 20
 21// coberturaXML is the root element of the Cobertura XML report.
 22type coberturaXML struct {
 23  XMLName      xml.Name          `xml:"coverage"`
 24  Version      string            `xml:"version,attr"`
 25  Timestamp    int64             `xml:"timestamp,attr"`
 26  LinesValid   int               `xml:"lines-valid,attr"`
 27  LinesCovered int               `xml:"lines-covered,attr"`
 28  LineRate     float64           `xml:"line-rate,attr"`
 29  Packages     coberturaPackages `xml:"packages"`
 30}
 31
 32type coberturaPackages struct {
 33  Packages []coberturaPackage `xml:"package"`
 34}
 35
 36type coberturaPackage struct {
 37  Name     string           `xml:"name,attr"`
 38  LineRate float64          `xml:"line-rate,attr"`
 39  Classes  coberturaClasses `xml:"classes"`
 40}
 41
 42type coberturaClasses struct {
 43  Classes []coberturaClass `xml:"class"`
 44}
 45
 46type coberturaClass struct {
 47  Name     string         `xml:"name,attr"`
 48  Filename string         `xml:"filename,attr"`
 49  LineRate float64        `xml:"line-rate,attr"`
 50  Lines    coberturaLines `xml:"lines"`
 51}
 52
 53type coberturaLines struct {
 54  Lines []coberturaLine `xml:"line"`
 55}
 56
 57type coberturaLine struct {
 58  Number int `xml:"number,attr"`
 59  Hits   int `xml:"hits,attr"`
 60}
 61
 62func (c *Cobertura) Write(_ context.Context, w io.Writer, _ *domain.SuiteResult, cov *domain.CoverageData) error {
 63  if cov == nil {
 64    cov = &domain.CoverageData{}
 65  }
 66
 67  lineRate := 0.0
 68  if cov.TotalLines() > 0 {
 69    lineRate = float64(cov.HitLines()) / float64(cov.TotalLines())
 70  }
 71
 72  report := coberturaXML{
 73    Version:      "neospec-1.0",
 74    Timestamp:    time.Now().Unix(),
 75    LinesValid:   cov.TotalLines(),
 76    LinesCovered: cov.HitLines(),
 77    LineRate:     lineRate,
 78  }
 79
 80  for _, file := range cov.Files {
 81    lineRate := 0.0
 82    if file.TotalLines() > 0 {
 83      lineRate = float64(file.HitLines()) / float64(file.TotalLines())
 84    }
 85
 86    lines := make([]int, 0, len(file.Lines))
 87    for ln := range file.Lines {
 88      lines = append(lines, ln)
 89    }
 90    sort.Ints(lines)
 91
 92    cls := coberturaClass{
 93      Name:     file.Path,
 94      Filename: file.Path,
 95      LineRate: lineRate,
 96    }
 97    for _, ln := range lines {
 98      cls.Lines.Lines = append(cls.Lines.Lines, coberturaLine{
 99        Number: ln,
 100        Hits:   file.Lines[ln],
 101      })
 102    }
 103
 104    pkg := coberturaPackage{
 105      Name:     ".",
 106      LineRate: lineRate,
 107    }
 108    pkg.Classes.Classes = append(pkg.Classes.Classes, cls)
 109    report.Packages.Packages = append(report.Packages.Packages, pkg)
 110  }
 111
 112  fmt.Fprintln(w, `<?xml version="1.0" encoding="UTF-8"?>`)
 113  enc := xml.NewEncoder(w)
 114  enc.Indent("", "  ")
 115  if err := enc.Encode(report); err != nil {
 116    return fmt.Errorf("encoding cobertura XML: %w", err)
 117  }
 118  return enc.Flush()
 119}

/home/runner/work/neospec/neospec/internal/adapters/reporter/console.go

#LineLine coverage
 1// Package reporter contains implementations of ports.Reporter for each
 2// supported output format.
 3package reporter
 4
 5import (
 6  "context"
 7  "fmt"
 8  "io"
 9
 10  "github.com/jedi-knights/neospec/internal/domain"
 11)
 12
 13// Console writes a human-readable test and coverage summary to the writer.
 14// It uses ANSI color codes when the writer is likely a TTY; callers that need
 15// plain text can wrap the writer with a color-stripping adapter.
 16type Console struct {
 17  // Color controls whether ANSI escape codes are emitted.
 18  Color bool
 19}
 20
 21// NewConsole creates a Console reporter. Pass color=true for terminal output.
 922func NewConsole(color bool) *Console {
 923  return &Console{Color: color}
 924}
 25
 26func (c *Console) Write(_ context.Context, w io.Writer, suite *domain.SuiteResult, cov *domain.CoverageData) error {
 27  pass, fail, skip, errors := suite.Counts()
 28
 29  for _, t := range suite.Tests {
 30    symbol, color := c.statusSymbol(t.Status)
 31    c.fprintColor(w, color, fmt.Sprintf("  %s %s\n", symbol, t.Name))
 32    if t.Error != "" {
 33      c.fprintColor(w, colorRed, fmt.Sprintf("    %s\n", t.Error))
 34    }
 35  }
 36
 37  fmt.Fprintln(w)
 38  summary := fmt.Sprintf(
 39    "Tests: %d passed, %d failed, %d skipped, %d errors  (%.2fs)\n",
 40    pass, fail, skip, errors,
 41    suite.Duration.Seconds(),
 42  )
 43  if fail > 0 || errors > 0 {
 44    c.fprintColor(w, colorRed, summary)
 45  } else {
 46    c.fprintColor(w, colorGreen, summary)
 47  }
 48
 49  if cov != nil && cov.TotalLines() > 0 {
 50    pct := cov.Percentage()
 51    covLine := fmt.Sprintf("Coverage: %s (%d/%d lines)\n",
 52      domain.BadgeLabel(pct), cov.HitLines(), cov.TotalLines())
 53    c.fprintColor(w, colorForPct(pct), covLine)
 54  }
 55
 56  return nil
 57}
 58
 59const (
 60  colorReset  = "\033[0m"
 61  colorRed    = "\033[31m"
 62  colorGreen  = "\033[32m"
 63  colorYellow = "\033[33m"
 64  colorOrange = "\033[38;5;208m"
 65)
 66
 767func colorForPct(pct float64) string {
 768  switch domain.BadgeColor(pct) {
 369  case "brightgreen", "green":
 370    return colorGreen
 171  case "yellow":
 172    return colorYellow
 273  case "orange":
 274    return colorOrange
 175  default:
 176    return colorRed
 77  }
 78}
 79
 80func (c *Console) fprintColor(w io.Writer, color, s string) {
 81  if c.Color {
 82    fmt.Fprintf(w, "%s%s%s", color, s, colorReset)
 83  } else {
 84    fmt.Fprint(w, s)
 85  }
 86}
 87
 88func (c *Console) statusSymbol(s domain.TestStatus) (string, string) {
 89  switch s {
 90  case domain.StatusPass:
 91    return "✓", colorGreen
 92  case domain.StatusFail:
 93    return "✗", colorRed
 94  case domain.StatusSkip:
 95    return "○", colorYellow
 96  default:
 97    return "!", colorOrange
 98  }
 99}

/home/runner/work/neospec/neospec/internal/adapters/reporter/coveralls.go

#LineLine coverage
 1package reporter
 2
 3import (
 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
 15type Coveralls struct{}
 16
 17// NewCoveralls creates a Coveralls reporter.
 418func NewCoveralls() *Coveralls { return &Coveralls{} }
 19
 20// coverallsPayload is the top-level Coveralls JSON structure.
 21type 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.
 30type coverallsSource struct {
 31  Name     string `json:"name"`
 32  Coverage []*int `json:"coverage"` // nil = not executable
 33}
 34
 35func (c *Coveralls) Write(_ context.Context, w io.Writer, _ *domain.SuiteResult, cov *domain.CoverageData) error {
 36  if cov == nil {
 37    cov = &domain.CoverageData{}
 38  }
 39
 40  payload := coverallsPayload{
 41    ServiceName: "neospec",
 42  }
 43
 44  for _, file := range cov.Files {
 45    if len(file.Lines) == 0 {
 46      continue
 47    }
 48
 49    // Find the maximum line number to size the coverage array.
 50    maxLine := 0
 51    lines := make([]int, 0, len(file.Lines))
 52    for ln := range file.Lines {
 53      lines = append(lines, ln)
 54      if ln > maxLine {
 55        maxLine = ln
 56      }
 57    }
 58    sort.Ints(lines)
 59
 60    // Coveralls coverage array is 0-indexed (line N is at index N-1).
 61    coverage := make([]*int, maxLine)
 62    for _, ln := range lines {
 63      hits := file.Lines[ln]
 64      coverage[ln-1] = &hits
 65    }
 66
 67    payload.SourceFiles = append(payload.SourceFiles, coverallsSource{
 68      Name:     file.Path,
 69      Coverage: coverage,
 70    })
 71  }
 72
 73  data, err := json.MarshalIndent(payload, "", "  ")
 74  if err != nil {
 75    return fmt.Errorf("marshaling coveralls JSON: %w", err)
 76  }
 77  _, err = fmt.Fprintln(w, string(data))
 78  return err
 79}

/home/runner/work/neospec/neospec/internal/adapters/reporter/junit.go

#LineLine coverage
 1package reporter
 2
 3import (
 4  "context"
 5  "encoding/xml"
 6  "fmt"
 7  "io"
 8  "time"
 9
 10  "github.com/jedi-knights/neospec/internal/domain"
 11)
 12
 13// JUnit writes test results in JUnit XML format.
 14// https://github.com/testmoapp/junitxml
 15// Coverage data is not included — JUnit is a test-results-only format.
 16type JUnit struct{}
 17
 18// NewJUnit creates a JUnit reporter.
 319func NewJUnit() *JUnit { return &JUnit{} }
 20
 21type junitTestSuites struct {
 22  XMLName    xml.Name         `xml:"testsuites"`
 23  Tests      int              `xml:"tests,attr"`
 24  Failures   int              `xml:"failures,attr"`
 25  Errors     int              `xml:"errors,attr"`
 26  Skipped    int              `xml:"skipped,attr"`
 27  Time       float64          `xml:"time,attr"`
 28  Timestamp  string           `xml:"timestamp,attr"`
 29  TestSuites []junitTestSuite `xml:"testsuite"`
 30}
 31
 32type junitTestSuite struct {
 33  Name      string          `xml:"name,attr"`
 34  Tests     int             `xml:"tests,attr"`
 35  Failures  int             `xml:"failures,attr"`
 36  Errors    int             `xml:"errors,attr"`
 37  Skipped   int             `xml:"skipped,attr"`
 38  Time      float64         `xml:"time,attr"`
 39  TestCases []junitTestCase `xml:"testcase"`
 40}
 41
 42type junitTestCase struct {
 43  Name    string        `xml:"name,attr"`
 44  Time    float64       `xml:"time,attr"`
 45  Failure *junitFailure `xml:"failure,omitempty"`
 46  Error   *junitError   `xml:"error,omitempty"`
 47  Skipped *junitSkipped `xml:"skipped,omitempty"`
 48}
 49
 50type junitFailure struct {
 51  Message string `xml:"message,attr"`
 52  Text    string `xml:",chardata"`
 53}
 54
 55type junitError struct {
 56  Message string `xml:"message,attr"`
 57  Text    string `xml:",chardata"`
 58}
 59
 60type junitSkipped struct{}
 61
 62func (j *JUnit) Write(_ context.Context, w io.Writer, suite *domain.SuiteResult, _ *domain.CoverageData) error {
 63  pass, fail, skip, errors := suite.Counts()
 64
 65  jSuite := junitTestSuite{
 66    Name:     "neospec",
 67    Tests:    len(suite.Tests),
 68    Failures: fail,
 69    Errors:   errors,
 70    Skipped:  skip,
 71    Time:     suite.Duration.Seconds(),
 72  }
 73
 74  for _, t := range suite.Tests {
 75    tc := junitTestCase{
 76      Name: t.Name,
 77      Time: t.Duration.Seconds(),
 78    }
 79    switch t.Status {
 80    case domain.StatusFail:
 81      tc.Failure = &junitFailure{Message: t.Error, Text: t.Error}
 82    case domain.StatusError:
 83      tc.Error = &junitError{Message: t.Error, Text: t.Error}
 84    case domain.StatusSkip:
 85      tc.Skipped = &junitSkipped{}
 86    }
 87    jSuite.TestCases = append(jSuite.TestCases, tc)
 88  }
 89
 90  root := junitTestSuites{
 91    Tests:      len(suite.Tests),
 92    Failures:   fail,
 93    Errors:     errors,
 94    Skipped:    skip,
 95    Time:       suite.Duration.Seconds(),
 96    Timestamp:  time.Now().UTC().Format(time.RFC3339),
 97    TestSuites: []junitTestSuite{jSuite},
 98  }
 99
 100  _ = pass // pass count is not a JUnit attribute at the suites level
 101
 102  fmt.Fprintln(w, `<?xml version="1.0" encoding="UTF-8"?>`)
 103  enc := xml.NewEncoder(w)
 104  enc.Indent("", "  ")
 105  if err := enc.Encode(root); err != nil {
 106    return fmt.Errorf("encoding junit XML: %w", err)
 107  }
 108  return enc.Flush()
 109}

/home/runner/work/neospec/neospec/internal/adapters/reporter/lcov.go

#LineLine coverage
 1package reporter
 2
 3import (
 4  "context"
 5  "fmt"
 6  "io"
 7  "sort"
 8
 9  "github.com/jedi-knights/neospec/internal/domain"
 10)
 11
 12// LCOV writes coverage data in LCOV tracefile format.
 13// See https://ltp.sourceforge.net/coverage/lcov/geninfo.1.php for the format.
 14// Test result data is not included in LCOV output — it is a coverage-only format.
 15type LCOV struct{}
 16
 17// NewLCOV creates an LCOV reporter.
 318func NewLCOV() *LCOV { return &LCOV{} }
 19
 20func (l *LCOV) Write(_ context.Context, w io.Writer, _ *domain.SuiteResult, cov *domain.CoverageData) error {
 21  if cov == nil {
 22    return nil
 23  }
 24
 25  for _, file := range cov.Files {
 26    // TN: test name (empty is valid)
 27    fmt.Fprintln(w, "TN:")
 28    fmt.Fprintf(w, "SF:%s\n", file.Path)
 29
 30    // Collect and sort line numbers for deterministic output.
 31    lines := make([]int, 0, len(file.Lines))
 32    for ln := range file.Lines {
 33      lines = append(lines, ln)
 34    }
 35    sort.Ints(lines)
 36
 37    for _, ln := range lines {
 38      fmt.Fprintf(w, "DA:%d,%d\n", ln, file.Lines[ln])
 39    }
 40
 41    fmt.Fprintf(w, "LH:%d\n", file.HitLines())
 42    fmt.Fprintf(w, "LF:%d\n", file.TotalLines())
 43    fmt.Fprintln(w, "end_of_record")
 44  }
 45
 46  return nil
 47}