< Summary - Neospec Coverage

Line coverage
90%
Covered lines: 245
Uncovered lines: 27
Coverable lines: 272
Total lines: 413
Line coverage: 90%
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: NewCacheCmd0%00100%
File 1: newCacheCleanCmd0%0072.73%
File 1: newCacheListCmd0%00100%
File 1: listCache0%00100%
File 1: dirSize0%0057.14%
File 1: formatBytes0%00100%
File 2: NewRunCmd0%0086.96%
File 2: runTests0%0080.85%
File 2: provisionNeovim0%00100%
File 2: executeTests0%00100%
File 2: emitReports0%00100%
File 2: writeReport0%0057.14%
File 2: checkThreshold0%00100%
File 2: reporterFor0%00100%
File 2: applyFlags0%00100%
File 3: NewVersionCmd0%00100%

File(s)

/home/runner/work/neospec/neospec/cmd/neospec/commands/cache.go

#LineLine coverage
 1package commands
 2
 3import (
 4  "fmt"
 5  "io/fs"
 6  "os"
 7  "path/filepath"
 8
 9  "github.com/spf13/cobra"
 10
 11  "github.com/jedi-knights/neospec/internal/config"
 12)
 13
 14// NewCacheCmd returns the `neospec cache` parent command with subcommands.
 115func NewCacheCmd() *cobra.Command {
 116  cmd := &cobra.Command{
 117    Use:   "cache",
 118    Short: "Manage the Neovim binary cache",
 119  }
 120  cmd.AddCommand(newCacheCleanCmd(), newCacheListCmd())
 121  return cmd
 122}
 23
 224func newCacheCleanCmd() *cobra.Command {
 225  return &cobra.Command{
 226    Use:   "clean",
 227    Short: "Remove all cached Neovim binaries",
 128    RunE: func(_ *cobra.Command, _ []string) error {
 129      cfg, _ := config.Load("neospec.toml")
 030      if err := os.RemoveAll(cfg.CacheDir); err != nil {
 031        return fmt.Errorf("cleaning cache: %w", err)
 032      }
 133      fmt.Printf("Removed cache directory: %s\n", cfg.CacheDir)
 134      return nil
 35    },
 36  }
 37}
 38
 239func newCacheListCmd() *cobra.Command {
 240  return &cobra.Command{
 241    Use:   "list",
 242    Short: "List cached Neovim versions and their sizes",
 143    RunE: func(_ *cobra.Command, _ []string) error {
 144      cfg, _ := config.Load("neospec.toml")
 145      return listCache(cfg.CacheDir)
 146    },
 47  }
 48}
 49
 550func listCache(cacheDir string) error {
 551  entries, err := os.ReadDir(cacheDir)
 152  if os.IsNotExist(err) {
 153    fmt.Println("Cache is empty.")
 154    return nil
 155  }
 156  if err != nil {
 157    return fmt.Errorf("reading cache dir: %w", err)
 158  }
 259  if len(entries) == 0 {
 260    fmt.Println("Cache is empty.")
 261    return nil
 262  }
 63
 164  fmt.Printf("%-20s  %s\n", "VERSION", "SIZE")
 165  fmt.Printf("%-20s  %s\n", "-------", "----")
 166  for _, e := range entries {
 167    if !e.IsDir() {
 168      continue
 69    }
 170    size, _ := dirSize(filepath.Join(cacheDir, e.Name()))
 171    fmt.Printf("%-20s  %s\n", e.Name(), formatBytes(size))
 72  }
 173  return nil
 74}
 75
 276func dirSize(root string) (int64, error) {
 277  var total int64
 278  err := filepath.WalkDir(root, func(_ string, d fs.DirEntry, err error) error {
 079    if err != nil {
 080      return err
 081    }
 382    if !d.IsDir() {
 383      info, err := d.Info()
 084      if err != nil {
 085        return err
 086      }
 387      total += info.Size()
 88    }
 589    return nil
 90  })
 291  return total, err
 92}
 93
 994func formatBytes(b int64) string {
 995  const unit = 1024
 496  if b < unit {
 497    return fmt.Sprintf("%d B", b)
 498  }
 599  div, exp := int64(unit), 0
 5100  for n := b / unit; n >= unit; n /= unit {
 5101    div *= unit
 5102    exp++
 5103  }
 5104  return fmt.Sprintf("%.1f %cB", float64(b)/float64(div), "KMGTPE"[exp])
 105}

/home/runner/work/neospec/neospec/cmd/neospec/commands/run.go

#LineLine coverage
 1// Package commands contains the cobra command implementations for the neospec CLI.
 2package commands
 3
 4import (
 5  "context"
 6  "fmt"
 7  "os"
 8  "path/filepath"
 9
 10  "github.com/spf13/cobra"
 11
 12  "github.com/jedi-knights/neospec/internal/adapters/neovim"
 13  "github.com/jedi-knights/neospec/internal/adapters/reporter"
 14  "github.com/jedi-knights/neospec/internal/adapters/runner"
 15  "github.com/jedi-knights/neospec/internal/config"
 16  "github.com/jedi-knights/neospec/internal/domain"
 17  "github.com/jedi-knights/neospec/internal/ports"
 18)
 19
 20// runDeps holds injectable dependencies for runTests. Pass runDeps{} in
 21// production — zero-value (nil) fields cause the real adapters to be constructed.
 22// In tests, set individual fields to inject fakes without touching the network.
 23//
 24// testRunner bypasses runner construction entirely — use it when you only need
 25// to control Discover/Run behaviour. runnerFactory intercepts the constructor
 26// call itself, letting tests verify that the correct arguments (nvimPath,
 27// verbose, initFile, coverageInclude) are threaded through from config.
 28type runDeps struct {
 29  neovimProvider ports.NeovimProvider
 30  testRunner     ports.TestRunner
 31  runnerFactory  func(nvimPath string, verbose bool, initFile string, coverageInclude []string) ports.TestRunner
 32}
 33
 34// runFlags holds values parsed from CLI flags for the run command.
 35type runFlags struct {
 36  configPath      string
 37  neovimVersion   string
 38  patterns        []string
 39  coverageDir     string
 40  formats         []string
 41  threshold       float64
 42  cacheDir        string
 43  verbose         bool
 44  initFile        string
 45  coverageInclude []string
 46}
 47
 48// NewRunCmd builds the `neospec run` (and default) command.
 249func NewRunCmd() *cobra.Command {
 250  flags := &runFlags{}
 251
 252  cmd := &cobra.Command{
 253    Use:   "run",
 254    Short: "Run Lua tests and collect coverage",
 255    Long:  `Discovers test files, executes them in an isolated Neovim subprocess, and emits reports.`,
 056    RunE: func(cmd *cobra.Command, _ []string) error {
 057      return runTests(cmd.Context(), flags, runDeps{})
 058    },
 59  }
 60
 261  f := cmd.Flags()
 262  f.StringVarP(&flags.configPath, "config", "c", "neospec.toml", "path to config file")
 263  f.StringVar(&flags.neovimVersion, "neovim-version", "", "neovim version to use (e.g. stable, nightly, v0.10.4)")
 264  f.StringArrayVar(&flags.patterns, "pattern", nil, "glob pattern(s) for test files (repeatable)")
 265  f.StringVar(&flags.coverageDir, "coverage-dir", "", "directory for coverage report files")
 266  f.StringArrayVar(&flags.formats, "format", nil, "output format(s): console, lcov, cobertura, coveralls, junit (repeata
 267  f.Float64Var(&flags.threshold, "threshold", 0, "minimum coverage percentage (0 = disabled; cannot clear a non-zero val
 268  f.StringVar(&flags.cacheDir, "cache-dir", "", "directory for cached Neovim binaries")
 269  f.BoolVarP(&flags.verbose, "verbose", "v", false, "verbose output")
 270  f.StringVar(&flags.initFile, "init-file", "", "path to a Lua file executed before the coverage hook (e.g. tests/minima
 271  f.StringArrayVar(&flags.coverageInclude, "coverage-include", nil, "restrict coverage to files whose path contains this
 272
 273  return cmd
 74}
 75
 1176func runTests(ctx context.Context, flags *runFlags, deps runDeps) error {
 1177  cfg, err := config.Load(flags.configPath)
 178  if err != nil {
 179    return fmt.Errorf("loading config: %w", err)
 180  }
 1081  applyFlags(&cfg, flags)
 1082
 1083  version, err := domain.ParseVersion(cfg.NeovimVersion)
 184  if err != nil {
 185    return fmt.Errorf("parsing neovim version: %w", err)
 186  }
 87
 988  platform, err := domain.CurrentPlatform()
 089  if err != nil {
 090    return fmt.Errorf("detecting platform: %w", err)
 091  }
 92
 993  nvimProvider := deps.neovimProvider
 094  if nvimProvider == nil {
 095    nvimProvider = neovim.NewProvider(cfg.CacheDir)
 096  }
 997  nvimPath, err := provisionNeovim(ctx, cfg, version, platform, nvimProvider)
 198  if err != nil {
 199    return err
 1100  }
 101
 8102  tr := deps.testRunner
 2103  if tr == nil {
 2104    if deps.runnerFactory != nil {
 2105      tr = deps.runnerFactory(nvimPath, cfg.Verbose, cfg.InitFile, cfg.CoverageInclude)
 0106    } else {
 0107      tr = runner.NewWithDefaultSandbox(nvimPath, cfg.Verbose, cfg.InitFile, cfg.CoverageInclude)
 0108    }
 109  }
 8110  suite, cov, err := executeTests(ctx, cfg, tr)
 1111  if err != nil {
 1112    return err
 1113  }
 3114  if suite == nil {
 3115    return nil // no test files found
 3116  }
 117
 1118  if err := emitReports(ctx, cfg, suite, cov); err != nil {
 1119    return err
 1120  }
 121
 1122  if err := checkThreshold(cfg, cov); err != nil {
 1123    return err
 1124  }
 125
 1126  if !suite.Passed() {
 1127    return fmt.Errorf("test suite failed")
 1128  }
 129
 1130  return nil
 131}
 132
 133// provisionNeovim calls provider.Ensure for the given version and platform and
 134// returns the path to the nvim binary. The provider is injected so tests can
 135// supply a fake without touching the filesystem or network.
 12136func provisionNeovim(ctx context.Context, cfg config.Config, version domain.Version, platform domain.Platform, provider 
 1137  if cfg.Verbose {
 1138    fmt.Fprintf(os.Stderr, "neospec: platform=%s neovim=%s\n", platform, version)
 1139  }
 12140  nvimPath, err := provider.Ensure(ctx, version, platform)
 2141  if err != nil {
 2142    return "", fmt.Errorf("ensuring neovim binary: %w", err)
 2143  }
 1144  if cfg.Verbose {
 1145    fmt.Fprintf(os.Stderr, "neospec: nvim binary at %s\n", nvimPath)
 1146  }
 10147  return nvimPath, nil
 148}
 149
 150// executeTests discovers and runs test files, then ensures the coverage output
 151// directory exists. It returns nil suite and coverage (with no error) when no
 152// test files are found so the caller can exit cleanly. The testRunner is injected
 153// so tests can supply a fake without spawning Neovim subprocesses.
 15154func executeTests(ctx context.Context, cfg config.Config, testRunner ports.TestRunner) (*domain.SuiteResult, *domain.Cov
 15155  files, err := testRunner.Discover(ctx, cfg.TestPatterns)
 2156  if err != nil {
 2157    return nil, nil, fmt.Errorf("discovering test files: %w", err)
 2158  }
 4159  if len(files) == 0 {
 4160    fmt.Fprintln(os.Stderr, "neospec: no test files found")
 4161    return nil, nil, nil
 4162  }
 1163  if cfg.Verbose {
 1164    fmt.Fprintf(os.Stderr, "neospec: found %d test file(s)\n", len(files))
 1165  }
 166
 9167  suite, cov, err := testRunner.Run(ctx, files)
 1168  if err != nil {
 1169    return nil, nil, fmt.Errorf("running tests: %w", err)
 1170  }
 171
 2172  if err := os.MkdirAll(cfg.CoverageDir, 0o755); err != nil {
 2173    return nil, nil, fmt.Errorf("creating coverage dir: %w", err)
 2174  }
 175
 6176  return suite, cov, nil
 177}
 178
 179// emitReports writes all configured output formats to their respective destinations.
 7180func emitReports(ctx context.Context, cfg config.Config, suite *domain.SuiteResult, cov *domain.CoverageData) error {
 7181  for _, format := range cfg.Formats {
 2182    if err := writeReport(ctx, cfg, format, suite, cov); err != nil {
 2183      return err
 2184    }
 185  }
 5186  return nil
 187}
 188
 189// writeReport opens, writes, and closes the output for a single report format.
 190// The file handle is deferred so it is closed even if r.Write panics.
 7191func writeReport(ctx context.Context, cfg config.Config, format string, suite *domain.SuiteResult, cov *domain.CoverageD
 7192  r, f, err := reporterFor(format, cfg, cfg.Verbose)
 2193  if err != nil {
 2194    return err
 2195  }
 1196  if f != nil && f != os.Stdout {
 1197    defer func() {
 0198      if cerr := f.Close(); cerr != nil && retErr == nil {
 0199        retErr = cerr
 0200      }
 201    }()
 202  }
 0203  if err := r.Write(ctx, f, suite, cov); err != nil {
 0204    return fmt.Errorf("writing %s report: %w", format, err)
 0205  }
 5206  return nil
 207}
 208
 209// checkThreshold returns an error if the measured coverage falls below the
 210// configured minimum. A threshold of zero disables the check. A nil cov is
 211// treated as 0% coverage — a conforming ports.TestRunner may return nil.
 7212func checkThreshold(cfg config.Config, cov *domain.CoverageData) error {
 3213  if cfg.Threshold <= 0 {
 3214    return nil
 3215  }
 4216  pct := 0.0
 3217  if cov != nil {
 3218    pct = cov.Percentage()
 3219  }
 3220  if pct < cfg.Threshold {
 3221    return fmt.Errorf("coverage %.1f%% is below threshold %.1f%%", pct, cfg.Threshold)
 3222  }
 1223  return nil
 224}
 225
 226// reporterFor returns the Reporter and output writer for a named format.
 227// For non-console formats the writer is a file in cfg.CoverageDir.
 17228func reporterFor(format string, cfg config.Config, color bool) (ports.Reporter, *os.File, error) {
 17229  switch format {
 5230  case "console":
 5231    return reporter.NewConsole(color), os.Stdout, nil
 3232  case "lcov":
 3233    f, err := os.Create(filepath.Join(cfg.CoverageDir, "lcov.info"))
 1234    if err != nil {
 1235      return nil, nil, fmt.Errorf("creating lcov report file: %w", err)
 1236    }
 2237    return reporter.NewLCOV(), f, nil
 2238  case "cobertura":
 2239    f, err := os.Create(filepath.Join(cfg.CoverageDir, "cobertura.xml"))
 1240    if err != nil {
 1241      return nil, nil, fmt.Errorf("creating cobertura report file: %w", err)
 1242    }
 1243    return reporter.NewCobertura(), f, nil
 2244  case "coveralls":
 2245    f, err := os.Create(filepath.Join(cfg.CoverageDir, "coveralls.json"))
 1246    if err != nil {
 1247      return nil, nil, fmt.Errorf("creating coveralls report file: %w", err)
 1248    }
 1249    return reporter.NewCoveralls(), f, nil
 2250  case "junit":
 2251    f, err := os.Create(filepath.Join(cfg.CoverageDir, "junit.xml"))
 1252    if err != nil {
 1253      return nil, nil, fmt.Errorf("creating junit report file: %w", err)
 1254    }
 1255    return reporter.NewJUnit(), f, nil
 3256  default:
 3257    return nil, nil, fmt.Errorf("unknown format %q: choose from console, lcov, cobertura, coveralls, junit", format)
 258  }
 259}
 260
 261// applyFlags overlays non-zero CLI flag values onto cfg.
 16262func applyFlags(cfg *config.Config, flags *runFlags) {
 2263  if flags.neovimVersion != "" {
 2264    cfg.NeovimVersion = flags.neovimVersion
 2265  }
 1266  if len(flags.patterns) > 0 {
 1267    cfg.TestPatterns = flags.patterns
 1268  }
 5269  if flags.coverageDir != "" {
 5270    cfg.CoverageDir = flags.coverageDir
 5271  }
 5272  if len(flags.formats) > 0 {
 5273    cfg.Formats = flags.formats
 5274  }
 2275  if flags.threshold > 0 {
 2276    cfg.Threshold = flags.threshold
 2277  }
 1278  if flags.cacheDir != "" {
 1279    cfg.CacheDir = flags.cacheDir
 1280  }
 1281  if flags.verbose {
 1282    cfg.Verbose = true
 1283  }
 3284  if flags.initFile != "" {
 3285    cfg.InitFile = flags.initFile
 3286  }
 1287  if len(flags.coverageInclude) > 0 {
 1288    cfg.CoverageInclude = flags.coverageInclude
 1289  }
 290}

/home/runner/work/neospec/neospec/cmd/neospec/commands/version.go

#LineLine coverage
 1package commands
 2
 3import (
 4  "fmt"
 5
 6  "github.com/spf13/cobra"
 7)
 8
 9// NewVersionCmd returns the `neospec version` command.
 210func NewVersionCmd(version string) *cobra.Command {
 211  return &cobra.Command{
 212    Use:   "version",
 213    Short: "Print neospec version",
 114    Run: func(_ *cobra.Command, _ []string) {
 115      fmt.Println("neospec", version)
 116    },
 17  }
 18}