< Summary - Neospec Coverage

Information
Line coverage
100%
Covered lines: 28
Uncovered lines: 0
Coverable lines: 28
Total lines: 185
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
VersionDir0%00100%
Lookup0%00100%
Extract0%00100%

File(s)

/home/runner/work/neospec/neospec/internal/adapters/neovim/cache.go

#LineLine coverage
 1package neovim
 2
 3import (
 4  "archive/tar"
 5  "archive/zip"
 6  "compress/gzip"
 7  "fmt"
 8  "io"
 9  "os"
 10  "path/filepath"
 11
 12  "github.com/jedi-knights/neospec/internal/domain"
 13)
 14
 15// Cache manages the on-disk store of extracted Neovim binaries.
 16// Layout: <cacheDir>/<version>/<platform>/bin/nvim[.exe]
 17type Cache struct {
 18  root string
 19}
 20
 21// NewCache creates a Cache rooted at the given directory.
 22func NewCache(root string) *Cache {
 23  return &Cache{root: root}
 24}
 25
 26// VersionDir returns the cache subdirectory for a version+platform pair.
 2527func (c *Cache) VersionDir(v domain.Version, p domain.Platform) string {
 2528  return filepath.Join(c.root, v.Tag, string(p.OS), string(p.Arch))
 2529}
 30
 31// Lookup checks whether a cached binary exists and returns its path.
 732func (c *Cache) Lookup(v domain.Version, p domain.Platform) (string, bool) {
 733  binPath := filepath.Join(c.VersionDir(v, p), "bin", domain.BinaryName(p))
 234  if _, err := os.Stat(binPath); err == nil {
 235    return binPath, true
 236  }
 537  return "", false
 38}
 39
 40// Extract unpacks the archive at archivePath into the cache directory for the
 41// given version and platform, and returns the path to the nvim binary.
 1442func (c *Cache) Extract(v domain.Version, p domain.Platform, archivePath string) (string, error) {
 1443  destDir := c.VersionDir(v, p)
 144  if err := os.MkdirAll(destDir, 0o755); err != nil {
 145    return "", fmt.Errorf("creating cache dir: %w", err)
 146  }
 47
 1348  switch p.OS {
 449  case domain.OSWindows:
 250    if err := extractZip(archivePath, destDir); err != nil {
 251      return "", err
 252    }
 953  default:
 354    if err := extractTarGz(archivePath, destDir); err != nil {
 355      return "", err
 356    }
 57  }
 58
 859  binPath := filepath.Join(destDir, "bin", domain.BinaryName(p))
 160  if _, err := os.Stat(binPath); err != nil {
 161    return "", fmt.Errorf("binary not found after extraction at %s: %w", binPath, err)
 162  }
 763  return binPath, nil
 64}
 65
 66// extractTarGz extracts a .tar.gz archive, stripping the first path component
 67// so that nvim-linux-x86_64/bin/nvim becomes <destDir>/bin/nvim.
 68func extractTarGz(archivePath, destDir string) error {
 69  f, err := os.Open(archivePath)
 70  if err != nil {
 71    return err
 72  }
 73  defer f.Close()
 74
 75  gz, err := gzip.NewReader(f)
 76  if err != nil {
 77    return fmt.Errorf("opening gzip stream: %w", err)
 78  }
 79  defer gz.Close()
 80
 81  tr := tar.NewReader(gz)
 82  for {
 83    hdr, err := tr.Next()
 84    if err == io.EOF {
 85      break
 86    }
 87    if err != nil {
 88      return fmt.Errorf("reading tar entry: %w", err)
 89    }
 90
 91    relPath := stripFirstComponent(hdr.Name)
 92    if relPath == "" {
 93      continue
 94    }
 95
 96    target := filepath.Join(destDir, relPath)
 97
 98    switch hdr.Typeflag {
 99    case tar.TypeDir:
 100      if err := os.MkdirAll(target, 0o755); err != nil {
 101        return err
 102      }
 103    case tar.TypeReg:
 104      if err := os.MkdirAll(filepath.Dir(target), 0o755); err != nil {
 105        return err
 106      }
 107      mode := os.FileMode(hdr.Mode)
 108      if err := writeFile(target, tr, mode); err != nil {
 109        return err
 110      }
 111    case tar.TypeSymlink:
 112      // Neovim archives include symlinks; recreate them.
 113      _ = os.Remove(target) // ignore error — may not exist
 114      if err := os.Symlink(hdr.Linkname, target); err != nil {
 115        return err
 116      }
 117    }
 118  }
 119  return nil
 120}
 121
 122// extractZip extracts a .zip archive (Windows), stripping the first component.
 123func extractZip(archivePath, destDir string) error {
 124  r, err := zip.OpenReader(archivePath)
 125  if err != nil {
 126    return err
 127  }
 128  defer r.Close()
 129
 130  for _, f := range r.File {
 131    relPath := stripFirstComponent(f.Name)
 132    if relPath == "" {
 133      continue
 134    }
 135    target := filepath.Join(destDir, relPath)
 136
 137    if f.FileInfo().IsDir() {
 138      if err := os.MkdirAll(target, 0o755); err != nil {
 139        return err
 140      }
 141      continue
 142    }
 143
 144    if err := os.MkdirAll(filepath.Dir(target), 0o755); err != nil {
 145      return err
 146    }
 147    rc, err := f.Open()
 148    if err != nil {
 149      return err
 150    }
 151    writeErr := writeFile(target, rc, f.Mode())
 152    if cerr := rc.Close(); cerr != nil && writeErr == nil {
 153      writeErr = cerr
 154    }
 155    if writeErr != nil {
 156      return writeErr
 157    }
 158  }
 159  return nil
 160}
 161
 162func writeFile(path string, r io.Reader, mode os.FileMode) (retErr error) {
 163  dst, err := os.OpenFile(path, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, mode)
 164  if err != nil {
 165    return err
 166  }
 167  defer func() {
 168    if cerr := dst.Close(); cerr != nil && retErr == nil {
 169      retErr = cerr
 170    }
 171  }()
 172  _, retErr = io.Copy(dst, r)
 173  return
 174}
 175
 176// stripFirstComponent removes the first path segment from a slash-separated
 177// path, e.g. "nvim-linux-x86_64/bin/nvim" → "bin/nvim".
 178func stripFirstComponent(p string) string {
 179  for i, c := range p {
 180    if (c == '/' || c == '\\') && i > 0 {
 181      return filepath.FromSlash(p[i+1:])
 182    }
 183  }
 184  return ""
 185}

Methods/Properties

VersionDir
Lookup
Extract