Skip to content

Commit 5d10563

Browse files
authored
Feat/ruby support (#46)
2 parents 70ac7fa + d086ec7 commit 5d10563

5 files changed

Lines changed: 112 additions & 1 deletion

File tree

internal/check/deps.go

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,8 @@ func (c *DepsCheck) Name() string {
4444
return "Python dependencies installed"
4545
case "go":
4646
return "Go dependencies installed"
47+
case "ruby":
48+
return "Ruby dependencies installed"
4749
default:
4850
return "Project dependencies installed"
4951
}
@@ -57,6 +59,8 @@ func (c *DepsCheck) Run(_ context.Context) Result {
5759
return c.runPython()
5860
case "go":
5961
return c.runGo()
62+
case "ruby":
63+
return c.runRuby()
6064
default:
6165
return Result{
6266
Name: c.Name(),
@@ -248,6 +252,34 @@ func parseFreeze(output []byte) map[string]struct{} {
248252
return installed
249253
}
250254

255+
func (c *DepsCheck) runRuby() Result {
256+
// vendor/bundle is the canonical signal that bundle install --path has been run.
257+
// Gemfile.lock is the fallback: it exists once bundle install has succeeded at least once.
258+
vendorBundle := filepath.Join(c.Dir, "vendor", "bundle")
259+
gemfileLock := filepath.Join(c.Dir, "Gemfile.lock")
260+
261+
if dirExists(vendorBundle) {
262+
return Result{
263+
Name: c.Name(),
264+
Status: StatusPass,
265+
Message: "vendor/bundle directory exists; Ruby gems are installed",
266+
}
267+
}
268+
if _, err := os.Stat(gemfileLock); err == nil {
269+
return Result{
270+
Name: c.Name(),
271+
Status: StatusPass,
272+
Message: "Gemfile.lock exists; Ruby gems have been installed",
273+
}
274+
}
275+
return Result{
276+
Name: c.Name(),
277+
Status: StatusFail,
278+
Message: "Gemfile.lock not found and vendor/bundle directory missing",
279+
Fix: "run `bundle install` to install Ruby gems",
280+
}
281+
}
282+
251283
func (c *DepsCheck) runGo() Result {
252284
vendorDir := filepath.Join(c.Dir, "vendor")
253285
if dirExists(vendorDir) {

internal/check/deps_test.go

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -245,4 +245,59 @@ func setupPythonDir(t *testing.T, requirements string) (dir string, pipBin strin
245245
t.Fatalf("write requirements.txt: %v", err)
246246
}
247247
return dir, pipBin
248+
}
249+
250+
func TestDepsCheck_Ruby_Pass_VendorBundle(t *testing.T) {
251+
dir := t.TempDir()
252+
if err := os.MkdirAll(filepath.Join(dir, "vendor", "bundle"), 0o755); err != nil {
253+
t.Fatalf("mkdir vendor/bundle: %v", err)
254+
}
255+
c := &DepsCheck{Dir: dir, Stack: "ruby"}
256+
r := c.Run(context.Background())
257+
if r.Status != StatusPass {
258+
t.Errorf("expected pass when vendor/bundle exists, got %v: %s", r.Status, r.Message)
259+
}
260+
}
261+
262+
func TestDepsCheck_Ruby_Pass_GemfileLock(t *testing.T) {
263+
dir := t.TempDir()
264+
if err := os.WriteFile(filepath.Join(dir, "Gemfile.lock"), []byte("GEM\n"), 0o644); err != nil {
265+
t.Fatalf("write Gemfile.lock: %v", err)
266+
}
267+
c := &DepsCheck{Dir: dir, Stack: "ruby"}
268+
r := c.Run(context.Background())
269+
if r.Status != StatusPass {
270+
t.Errorf("expected pass when Gemfile.lock exists, got %v: %s", r.Status, r.Message)
271+
}
272+
}
273+
274+
func TestDepsCheck_Ruby_Fail_NeitherPresent(t *testing.T) {
275+
dir := t.TempDir()
276+
c := &DepsCheck{Dir: dir, Stack: "ruby"}
277+
r := c.Run(context.Background())
278+
if r.Status != StatusFail {
279+
t.Errorf("expected fail when neither vendor/bundle nor Gemfile.lock exist, got %v", r.Status)
280+
}
281+
if !strings.Contains(r.Fix, "bundle install") {
282+
t.Errorf("expected fix to mention 'bundle install', got: %s", r.Fix)
283+
}
284+
}
285+
286+
func TestDepsCheck_Ruby_VendorBundle_Takes_Priority(t *testing.T) {
287+
// Both present — vendor/bundle should win (pass with that message).
288+
dir := t.TempDir()
289+
if err := os.MkdirAll(filepath.Join(dir, "vendor", "bundle"), 0o755); err != nil {
290+
t.Fatalf("mkdir vendor/bundle: %v", err)
291+
}
292+
if err := os.WriteFile(filepath.Join(dir, "Gemfile.lock"), []byte("GEM\n"), 0o644); err != nil {
293+
t.Fatalf("write Gemfile.lock: %v", err)
294+
}
295+
c := &DepsCheck{Dir: dir, Stack: "ruby"}
296+
r := c.Run(context.Background())
297+
if r.Status != StatusPass {
298+
t.Errorf("expected pass, got %v: %s", r.Status, r.Message)
299+
}
300+
if !strings.Contains(r.Message, "vendor/bundle") {
301+
t.Errorf("expected vendor/bundle message to take priority, got: %s", r.Message)
302+
}
248303
}

internal/check/registry.go

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,11 @@ func Build(stack detector.DetectedStack) []Check {
3838
cs = append(cs, &DepsCheck{Dir: ".", Stack: "python"})
3939
cs = append(cs, &GitHooksCheck{Dir: ".", Stack: "python"})
4040
}
41+
if stack.Ruby {
42+
cs = append(cs, &BinaryCheck{Binary: "ruby"})
43+
cs = append(cs, &BinaryCheck{Binary: "bundler"})
44+
cs = append(cs, &DepsCheck{Dir: ".", Stack: "ruby"})
45+
}
4146
if stack.Java {
4247
cs = append(cs, &BinaryCheck{Binary: "java"})
4348
if stack.Maven {
@@ -122,4 +127,4 @@ func Build(stack detector.DetectedStack) []Check {
122127
func fileExists(path string) bool {
123128
_, err := os.Stat(path)
124129
return err == nil
125-
}
130+
}

internal/detector/detector.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ type DetectedStack struct {
1414
// Possible values: "npm", "pnpm", "yarn". Empty string when Node is false.
1515
PackageManager string
1616
Python bool
17+
Ruby bool
1718
Java bool
1819
Maven bool
1920
Gradle bool
@@ -44,6 +45,7 @@ func Detect(dir string) DetectedStack {
4445
}
4546
stack.Python = fileExists(filepath.Join(dir, "requirements.txt")) ||
4647
fileExists(filepath.Join(dir, "pyproject.toml"))
48+
stack.Ruby = fileExists(filepath.Join(dir, "Gemfile"))
4749
stack.Maven = fileExists(filepath.Join(dir, "pom.xml"))
4850
stack.Gradle = fileExists(filepath.Join(dir, "build.gradle"))
4951
stack.Java = stack.Maven || stack.Gradle

internal/detector/detector_test.go

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,4 +140,21 @@ func TestDetect_GoWork_false_when_absent(t *testing.T) {
140140
if stack.GoWork {
141141
t.Error("expected GoWork=false when go.work is absent")
142142
}
143+
}
144+
145+
func TestDetect_Ruby_true_when_gemfile_present(t *testing.T) {
146+
dir := t.TempDir()
147+
touch(t, filepath.Join(dir, "Gemfile"))
148+
stack := Detect(dir)
149+
if !stack.Ruby {
150+
t.Error("expected Ruby=true when Gemfile exists")
151+
}
152+
}
153+
154+
func TestDetect_Ruby_false_when_absent(t *testing.T) {
155+
dir := t.TempDir()
156+
stack := Detect(dir)
157+
if stack.Ruby {
158+
t.Error("expected Ruby=false when no Gemfile present")
159+
}
143160
}

0 commit comments

Comments
 (0)