< Summary - Neospec Coverage

Information
Line coverage
100%
Covered lines: 88
Uncovered lines: 0
Coverable lines: 88
Total lines: 187
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
defaults0%00100%
Load0%00100%
loadTOML0%00100%
applyEnv0%00100%
splitTrimmed0%00100%
userCacheDirWith0%00100%

File(s)

/home/runner/work/neospec/neospec/internal/config/config.go

#LineLine coverage
 1// Package config loads and merges neospec configuration from three sources:
 2// a TOML file, environment variables, and CLI flags. Precedence is:
 3// CLI flags > environment variables > TOML file > built-in defaults.
 4package config
 5
 6import (
 7  "errors"
 8  "fmt"
 9  "io/fs"
 10  "os"
 11  "path/filepath"
 12  "strconv"
 13  "strings"
 14
 15  "github.com/BurntSushi/toml"
 16)
 17
 18// Config is the merged, validated configuration for a neospec run.
 19type Config struct {
 20  // NeovimVersion is the version tag to download, e.g. "stable", "nightly", or "v0.10.4".
 21  NeovimVersion string `toml:"neovim_version"`
 22  // TestPatterns are glob patterns for discovering test files.
 23  TestPatterns []string `toml:"test_patterns"`
 24  // CoverageDir is the directory where coverage report files are written.
 25  CoverageDir string `toml:"coverage_dir"`
 26  // Formats lists the report formats to emit: lcov, cobertura, coveralls, junit, console.
 27  Formats []string `toml:"formats"`
 28  // Threshold is the minimum required coverage percentage; a non-zero value
 29  // causes neospec to exit non-zero when coverage falls below it.
 30  Threshold float64 `toml:"threshold"`
 31  // CacheDir is the directory where downloaded Neovim binaries are stored.
 32  CacheDir string `toml:"cache_dir"`
 33  // Verbose enables additional diagnostic output.
 34  Verbose bool `toml:"verbose"`
 35  // InitFile is an optional path to a Lua file executed before the coverage
 36  // hook and test harness. Use it to set up runtimepath, mock dependencies, or
 37  // otherwise configure the Neovim environment before tests run. The file is
 38  // not instrumented by the coverage hook because it executes before the hook
 39  // is installed.
 40  InitFile string `toml:"init_file"`
 41  // CoverageInclude is an optional list of path substrings. When non-empty,
 42  // only source files whose absolute path contains at least one of these
 43  // strings are recorded. Use it to restrict coverage to your plugin's own
 44  // source tree and exclude Neovim's internal runtime files.
 45  // Example: coverage_include = ["lua/", "plugin/"]
 46  CoverageInclude []string `toml:"coverage_include"`
 47}
 48
 49// defaults returns a Config populated with built-in default values.
 2050func defaults() Config {
 2051  cacheDir := filepath.Join(userCacheDirWith(defaultOSDirs), "neospec")
 2052  return Config{
 2053    NeovimVersion: "stable",
 2054    TestPatterns:  []string{"test/**/*_spec.lua"},
 2055    CoverageDir:   "coverage",
 2056    Formats:       []string{"console"},
 2057    Threshold:     0.0,
 2058    CacheDir:      cacheDir,
 2059    Verbose:       false,
 2060  }
 2061}
 62
 63// Load reads neospec.toml from path (if it exists), then overlays environment
 64// variables, and returns the merged Config. CLI flag overrides are applied
 65// separately via the Apply* methods.
 2066func Load(path string) (Config, error) {
 2067  cfg := defaults()
 2068
 669  if path != "" {
 270    if err := loadTOML(path, &cfg); err != nil {
 271      return cfg, err
 272    }
 73  }
 74
 275  if err := applyEnv(&cfg); err != nil {
 276    return cfg, err
 277  }
 1678  return cfg, nil
 79}
 80
 81// loadTOML reads a TOML file into cfg. Missing files are silently ignored;
 82// malformed files return an error.
 683func loadTOML(path string, cfg *Config) error {
 684  data, err := os.ReadFile(path)
 185  if errors.Is(err, fs.ErrNotExist) {
 186    return nil
 187  }
 188  if err != nil {
 189    return fmt.Errorf("reading config file %s: %w", path, err)
 190  }
 191  if err := toml.Unmarshal(data, cfg); err != nil {
 192    return fmt.Errorf("parsing config file %s: %w", path, err)
 193  }
 394  return nil
 95}
 96
 97// applyEnv overlays environment variables onto cfg. Only non-empty env vars
 98// override the current value. It returns an error if a numeric env var is
 99// present but cannot be parsed, so user configuration mistakes are surfaced
 100// immediately rather than silently running with a different value.
 18101func applyEnv(cfg *Config) error {
 1102  if v := strings.TrimSpace(os.Getenv("NEOSPEC_NEOVIM_VERSION")); v != "" {
 1103    cfg.NeovimVersion = v
 1104  }
 1105  if v := strings.TrimSpace(os.Getenv("NEOSPEC_TEST_PATTERNS")); v != "" {
 1106    cfg.TestPatterns = splitTrimmed(v, ",")
 1107  }
 1108  if v := strings.TrimSpace(os.Getenv("NEOSPEC_COVERAGE_DIR")); v != "" {
 1109    cfg.CoverageDir = v
 1110  }
 3111  if v := strings.TrimSpace(os.Getenv("NEOSPEC_FORMATS")); v != "" {
 3112    cfg.Formats = splitTrimmed(v, ",")
 3113  }
 1114  if v := strings.TrimSpace(os.Getenv("NEOSPEC_CACHE_DIR")); v != "" {
 1115    cfg.CacheDir = v
 1116  }
 4117  if v := strings.TrimSpace(os.Getenv("NEOSPEC_VERBOSE")); v != "" {
 4118    switch v {
 2119    case "true", "1":
 2120      cfg.Verbose = true
 1121    case "false", "0":
 1122      cfg.Verbose = false
 1123    default:
 1124      return fmt.Errorf("NEOSPEC_VERBOSE %q is not a valid boolean (use true, false, 1, or 0; values are case-sensitive)
 125    }
 126  }
 3127  if v := strings.TrimSpace(os.Getenv("NEOSPEC_THRESHOLD")); v != "" {
 3128    f, err := strconv.ParseFloat(v, 64)
 1129    if err != nil {
 1130      return fmt.Errorf("NEOSPEC_THRESHOLD %q is not a valid float: %w", v, err)
 1131    }
 2132    cfg.Threshold = f
 133  }
 2134  if v := strings.TrimSpace(os.Getenv("NEOSPEC_INIT_FILE")); v != "" {
 2135    cfg.InitFile = v
 2136  }
 1137  if v := strings.TrimSpace(os.Getenv("NEOSPEC_COVERAGE_INCLUDE")); v != "" {
 1138    cfg.CoverageInclude = splitTrimmed(v, ",")
 1139  }
 16140  return nil
 141}
 142
 143// splitTrimmed splits s by sep, trims whitespace from each element, and
 144// removes empty elements. This handles CI-injected env vars like "lcov, junit"
 145// (space after comma) and trailing commas like "lcov," that would otherwise
 146// produce empty elements silently failing format matching at runtime.
 5147func splitTrimmed(s, sep string) []string {
 5148  parts := strings.Split(s, sep)
 5149  out := make([]string, 0, len(parts))
 5150  for _, p := range parts {
 9151    if t := strings.TrimSpace(p); t != "" {
 9152      out = append(out, t)
 9153    }
 154  }
 5155  return out
 156}
 157
 158// osDirs bundles the OS directory functions used by defaults(). Holding them
 159// in a struct lets tests inject controlled implementations without changing
 160// the exported API or relying on OS-level manipulation.
 161type osDirs struct {
 162  userCacheDir func() (string, error)
 163  userHomeDir  func() (string, error)
 164  tempDir      func() string
 165}
 166
 167// defaultOSDirs is the production set of OS directory lookups.
 168var defaultOSDirs = osDirs{
 169  userCacheDir: os.UserCacheDir,
 170  userHomeDir:  os.UserHomeDir,
 171  tempDir:      os.TempDir,
 172}
 173
 174// userCacheDirWith returns an absolute cache directory path using the
 175// provided OS directory functions, with progressive fallbacks. On minimal
 176// systems (containers without a passwd entry), both UserCacheDir and
 177// UserHomeDir may fail; in that case we fall back to the OS temp directory,
 178// which is guaranteed to be absolute.
 23179func userCacheDirWith(d osDirs) string {
 21180  if dir, err := d.userCacheDir(); err == nil {
 21181    return dir
 21182  }
 1183  if home, err := d.userHomeDir(); err == nil {
 1184    return filepath.Join(home, ".cache")
 1185  }
 1186  return filepath.Join(d.tempDir(), "neospec")
 187}