diff --git a/internal/check/deps.go b/internal/check/deps.go index b6256c4..2c01fb2 100644 --- a/internal/check/deps.go +++ b/internal/check/deps.go @@ -44,6 +44,8 @@ func (c *DepsCheck) Name() string { return "Python dependencies installed" case "go": return "Go dependencies installed" + case "ruby": + return "Ruby dependencies installed" default: return "Project dependencies installed" } @@ -57,6 +59,8 @@ func (c *DepsCheck) Run(_ context.Context) Result { return c.runPython() case "go": return c.runGo() + case "ruby": + return c.runRuby() default: return Result{ Name: c.Name(), @@ -248,6 +252,34 @@ func parseFreeze(output []byte) map[string]struct{} { return installed } +func (c *DepsCheck) runRuby() Result { + // vendor/bundle is the canonical signal that bundle install --path has been run. + // Gemfile.lock is the fallback: it exists once bundle install has succeeded at least once. + vendorBundle := filepath.Join(c.Dir, "vendor", "bundle") + gemfileLock := filepath.Join(c.Dir, "Gemfile.lock") + + if dirExists(vendorBundle) { + return Result{ + Name: c.Name(), + Status: StatusPass, + Message: "vendor/bundle directory exists; Ruby gems are installed", + } + } + if _, err := os.Stat(gemfileLock); err == nil { + return Result{ + Name: c.Name(), + Status: StatusPass, + Message: "Gemfile.lock exists; Ruby gems have been installed", + } + } + return Result{ + Name: c.Name(), + Status: StatusFail, + Message: "Gemfile.lock not found and vendor/bundle directory missing", + Fix: "run `bundle install` to install Ruby gems", + } +} + func (c *DepsCheck) runGo() Result { vendorDir := filepath.Join(c.Dir, "vendor") if dirExists(vendorDir) { diff --git a/internal/check/deps_test.go b/internal/check/deps_test.go index 06376a5..7fb7d41 100644 --- a/internal/check/deps_test.go +++ b/internal/check/deps_test.go @@ -245,4 +245,59 @@ func setupPythonDir(t *testing.T, requirements string) (dir string, pipBin strin t.Fatalf("write requirements.txt: %v", err) } return dir, pipBin +} + +func TestDepsCheck_Ruby_Pass_VendorBundle(t *testing.T) { + dir := t.TempDir() + if err := os.MkdirAll(filepath.Join(dir, "vendor", "bundle"), 0o755); err != nil { + t.Fatalf("mkdir vendor/bundle: %v", err) + } + c := &DepsCheck{Dir: dir, Stack: "ruby"} + r := c.Run(context.Background()) + if r.Status != StatusPass { + t.Errorf("expected pass when vendor/bundle exists, got %v: %s", r.Status, r.Message) + } +} + +func TestDepsCheck_Ruby_Pass_GemfileLock(t *testing.T) { + dir := t.TempDir() + if err := os.WriteFile(filepath.Join(dir, "Gemfile.lock"), []byte("GEM\n"), 0o644); err != nil { + t.Fatalf("write Gemfile.lock: %v", err) + } + c := &DepsCheck{Dir: dir, Stack: "ruby"} + r := c.Run(context.Background()) + if r.Status != StatusPass { + t.Errorf("expected pass when Gemfile.lock exists, got %v: %s", r.Status, r.Message) + } +} + +func TestDepsCheck_Ruby_Fail_NeitherPresent(t *testing.T) { + dir := t.TempDir() + c := &DepsCheck{Dir: dir, Stack: "ruby"} + r := c.Run(context.Background()) + if r.Status != StatusFail { + t.Errorf("expected fail when neither vendor/bundle nor Gemfile.lock exist, got %v", r.Status) + } + if !strings.Contains(r.Fix, "bundle install") { + t.Errorf("expected fix to mention 'bundle install', got: %s", r.Fix) + } +} + +func TestDepsCheck_Ruby_VendorBundle_Takes_Priority(t *testing.T) { + // Both present — vendor/bundle should win (pass with that message). + dir := t.TempDir() + if err := os.MkdirAll(filepath.Join(dir, "vendor", "bundle"), 0o755); err != nil { + t.Fatalf("mkdir vendor/bundle: %v", err) + } + if err := os.WriteFile(filepath.Join(dir, "Gemfile.lock"), []byte("GEM\n"), 0o644); err != nil { + t.Fatalf("write Gemfile.lock: %v", err) + } + c := &DepsCheck{Dir: dir, Stack: "ruby"} + r := c.Run(context.Background()) + if r.Status != StatusPass { + t.Errorf("expected pass, got %v: %s", r.Status, r.Message) + } + if !strings.Contains(r.Message, "vendor/bundle") { + t.Errorf("expected vendor/bundle message to take priority, got: %s", r.Message) + } } \ No newline at end of file diff --git a/internal/check/registry.go b/internal/check/registry.go index 0b9aab8..af3655a 100644 --- a/internal/check/registry.go +++ b/internal/check/registry.go @@ -38,6 +38,11 @@ func Build(stack detector.DetectedStack) []Check { cs = append(cs, &DepsCheck{Dir: ".", Stack: "python"}) cs = append(cs, &GitHooksCheck{Dir: ".", Stack: "python"}) } + if stack.Ruby { + cs = append(cs, &BinaryCheck{Binary: "ruby"}) + cs = append(cs, &BinaryCheck{Binary: "bundler"}) + cs = append(cs, &DepsCheck{Dir: ".", Stack: "ruby"}) + } if stack.Java { cs = append(cs, &BinaryCheck{Binary: "java"}) if stack.Maven { @@ -122,4 +127,4 @@ func Build(stack detector.DetectedStack) []Check { func fileExists(path string) bool { _, err := os.Stat(path) return err == nil -} +} \ No newline at end of file diff --git a/internal/detector/detector.go b/internal/detector/detector.go index 1ee7f21..aaefd32 100644 --- a/internal/detector/detector.go +++ b/internal/detector/detector.go @@ -14,6 +14,7 @@ type DetectedStack struct { // Possible values: "npm", "pnpm", "yarn". Empty string when Node is false. PackageManager string Python bool + Ruby bool Java bool Maven bool Gradle bool @@ -44,6 +45,7 @@ func Detect(dir string) DetectedStack { } stack.Python = fileExists(filepath.Join(dir, "requirements.txt")) || fileExists(filepath.Join(dir, "pyproject.toml")) + stack.Ruby = fileExists(filepath.Join(dir, "Gemfile")) stack.Maven = fileExists(filepath.Join(dir, "pom.xml")) stack.Gradle = fileExists(filepath.Join(dir, "build.gradle")) stack.Java = stack.Maven || stack.Gradle diff --git a/internal/detector/detector_test.go b/internal/detector/detector_test.go index d8d5a3b..feebedd 100644 --- a/internal/detector/detector_test.go +++ b/internal/detector/detector_test.go @@ -140,4 +140,21 @@ func TestDetect_GoWork_false_when_absent(t *testing.T) { if stack.GoWork { t.Error("expected GoWork=false when go.work is absent") } +} + +func TestDetect_Ruby_true_when_gemfile_present(t *testing.T) { + dir := t.TempDir() + touch(t, filepath.Join(dir, "Gemfile")) + stack := Detect(dir) + if !stack.Ruby { + t.Error("expected Ruby=true when Gemfile exists") + } +} + +func TestDetect_Ruby_false_when_absent(t *testing.T) { + dir := t.TempDir() + stack := Detect(dir) + if stack.Ruby { + t.Error("expected Ruby=false when no Gemfile present") + } } \ No newline at end of file