Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -296,7 +296,7 @@ Language ecosystems and development tools across multiple categories.

**Languages:** Ada, C, C#, C++, COBOL, Clojure, Common Lisp, Crystal, D, Dart, Deno, Elixir, Elm, Emacs Lisp, Erlang, F#, Fortran, GDScript, Gleam, Go, Groovy, Haskell, Haxe, Java, JavaScript, Julia, Kotlin, Lua, Mojo, Nim, Nix, OCaml, Objective-C, Odin, PHP, Perl, Prolog, Python, R, Racket, Roc, Ruby, Rust, Scala, Scheme, Solidity, Swift, Tcl, TypeScript, V, VHDL, Verilog, Zig.

**Package Managers:** Alire, Bun, Bundler, Cabal, Cargo, CocoaPods, Composer, Conan, Conda, DUB, Deno Modules, Flit, Gleam Packages, Go Modules, Gradle, Hatch, Haxelib, Jsonnet Bundler, Maven, Mint, Mix, Nix Flakes, NuGet, PDM, Pipenv, Pkg, Poetry, Quicklisp, Shards, Stack, Swift Package Manager, Yarn, cpanm, dotnet CLI, elm, fpm, npm, opam, pip, pnpm, pub, rebar3, renv, sbt, setuptools, uv, vcpkg.
**Package Managers:** Alire, Bun, Bundler, Cabal, Cargo, CocoaPods, Composer, Conan, Conda, DUB, Deno Modules, Flit, Gleam Packages, Go Modules, Gradle, Hatch, Haxelib, Jsonnet Bundler, Maven, Mint, Mix, Nix Flakes, NuGet, PDM, Pipenv, Pkg, Poetry, Quicklisp, RubyGems, Shards, Stack, Swift Package Manager, Yarn, cpanm, dotnet CLI, elm, fpm, npm, opam, pip, pnpm, pub, rebar3, renv, sbt, setuptools, uv, vcpkg.

**Test:** ASV, AVA, Alcotest, Artillery, Bats, BenchmarkTools.jl, Bruno, Catch2, Cucumber, Cypress, EUnit, ExUnit, Gatling, Ginkgo, Google Test, Hspec, Hurl, JUnit, Jasmine, Jest, Kotest, Lighthouse CI, Locust, MSW, Minitest, Mocha, Newman, PHPUnit, Pest, Playwright, REST Client, RSpec, ScalaTest, Selenium, Testify, Testing Library, Vitest, XCTest, axe-core, benchmark-ips, cargo test, clojure.test, criterion, crystal spec, dart test, deno test, dotnet test, gleam test, go test, hyperfine, k6, kotlin.test, nf-test, pytest, pytest-benchmark, tape, testament, testthat, tinytest, tox, vdiffr, zig test.

Expand Down
61 changes: 61 additions & 0 deletions cmd/brief/ruby_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package main

import (
"encoding/json"
"os"
"os/exec"
"path/filepath"
"testing"

"github.com/git-pkgs/brief"
)

func TestScanRubyPackageManagers(t *testing.T) {
const helperEnv = "BRIEF_RUBY_PACKAGE_MANAGER_ROOT"
if root := os.Getenv(helperEnv); root != "" {
cmdScan([]string{"-json", root})
return
}
for _, tt := range []struct {
name string
path string
files map[string]string
manager string
lockfile string
}{
{"gem subdirectory", "packages/parser", map[string]string{
"Gemfile": "source 'https://rubygems.org'\ngem 'rubocop'\n",
"packages/parser/parser.gemspec": "Gem::Specification.new do |spec|\n spec.name = 'parser'\nend\n",
}, "RubyGems", ""},
{"alternate Bundler layout", ".", map[string]string{
"gems.rb": "source 'https://rubygems.org'\ngem 'rake'\n",
"gems.locked": "GEM\n specs:\n rake (13.0.0)\n\nDEPENDENCIES\n rake\n",
}, "Bundler", "gems.locked"},
{"Hoe project", ".", map[string]string{
"Rakefile": "require 'hoe'\nHoe.spec 'parser' do\nend\n",
}, "RubyGems", ""},
} {
t.Run(tt.name, func(t *testing.T) {
root := t.TempDir()
for path, content := range tt.files {
writeScanFixture(t, root, path, content)
}
cmd := exec.Command(os.Args[0], "-test.run=^TestScanRubyPackageManagers$")
cmd.Env = append(os.Environ(), helperEnv+"="+filepath.Join(root, tt.path), "PATH=")
out, err := cmd.Output()
if err != nil {
t.Fatalf("scan failed: %v", err)
}
var report brief.Report
if err := json.Unmarshal(out, &report); err != nil {
t.Fatalf("parsing scan output: %v\n%s", err, out)
}
if len(report.PackageManagers) != 1 || report.PackageManagers[0].Name != tt.manager {
t.Fatalf("package managers = %+v, want %s", report.PackageManagers, tt.manager)
}
if got := report.PackageManagers[0].Lockfile; got != tt.lockfile {
t.Errorf("lockfile = %q, want %q", got, tt.lockfile)
}
})
}
}
53 changes: 53 additions & 0 deletions detect/bundler.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package detect

import (
"path"
"path/filepath"
"slices"

"github.com/git-pkgs/brief/kb"
)

const (
bundlerGemfile = "Gemfile"
bundlerGemfileLock = "Gemfile.lock"
bundlerGemsRB = "gems.rb"
bundlerGemsLocked = "gems.locked"
)

func (e *Engine) bundlerFiles(root string) (manifest, lockfile string) {
root = filepath.ToSlash(root)
if e.exactFileExists(path.Join(root, bundlerGemsRB)) {
return path.Join(root, bundlerGemsRB), path.Join(root, bundlerGemsLocked)
}
return path.Join(root, bundlerGemfile), path.Join(root, bundlerGemfileLock)
}

func (e *Engine) activeBundlerFile(root, file string) bool {
switch file {
case bundlerGemfile, bundlerGemfileLock, bundlerGemsRB, bundlerGemsLocked:
manifest, lockfile := e.bundlerFiles(root)
candidate := path.Join(filepath.ToSlash(root), file)
return candidate == manifest || candidate == lockfile
default:
return true
}
}

func (e *Engine) detectLockfile(tool *kb.ToolDef) string {
if slices.Contains(tool.Config.Files, bundlerGemsRB) {
for _, root := range e.analysisRoots() {
_, lockfile := e.bundlerFiles(root)
if e.exactFileExists(lockfile) {
return lockfile
}
}
return ""
}
if tool.Config.Lockfile != "" {
if found := e.findExisting([]string{tool.Config.Lockfile}); len(found) > 0 {
return found[0]
}
}
return ""
}
9 changes: 4 additions & 5 deletions detect/detect.go
Original file line number Diff line number Diff line change
Expand Up @@ -554,11 +554,7 @@ func (e *Engine) detectCategory(category string) []brief.Detection {

d.ConfigFiles = e.findExisting(tool.Config.Files)

if tool.Config.Lockfile != "" {
if lockfiles := e.findExisting([]string{tool.Config.Lockfile}); len(lockfiles) > 0 {
d.Lockfile = lockfiles[0]
}
}
d.Lockfile = e.detectLockfile(tool)

detections = append(detections, d)
}
Expand Down Expand Up @@ -1136,6 +1132,9 @@ func (e *Engine) manifestPaths() []string {
roots := e.analysisRoots()
for _, root := range roots {
for _, mf := range e.KB.ManifestFiles {
if !e.activeBundlerFile(root, mf) {
continue
}
add(path.Join(filepath.ToSlash(root), mf))
}
}
Expand Down
118 changes: 118 additions & 0 deletions detect/ruby_package_manager_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
package detect

import (
"slices"
"testing"

"github.com/git-pkgs/brief"
)

func TestRubyGemsPackageManager(t *testing.T) {
t.Setenv("PATH", "")
for name, files := range map[string]map[string]string{
"gemspec only": {"parser.gemspec": "Gem::Specification.new do |spec|\n spec.name = 'parser'\nend\n"},
"nested gemspec": {"packages/parser/parser.gemspec": "Gem::Specification.new do |spec|\n spec.name = 'parser'\nend\n"},
"Hoe": {"Rakefile": "require 'hoe'\nHoe.spec 'parser' do\nend\n"},
"lowercase Rakefile": {"rakefile": "require 'hoe'\nHoe.spec 'parser' do\nend\n"},
"legacy Hoe": {"Rakefile": "require 'hoe'\nHoe.new 'parser', '0.1.0'\n"},
"package task": {"Rakefile": "require 'rubygems/package_task'\nGem::PackageTask.new(spec)\n"},
"plain Ruby": {"main.rb": "puts 'hello'\n"},
"ordinary Rakefile": {"Rakefile": "task :test do\n ruby 'test.rb'\nend\n"},
} {
t.Run(name, func(t *testing.T) {
dir := t.TempDir()
for path, content := range files {
writeProjectFile(t, dir, path, content)
}
r := runOn(t, dir)
if !slices.Contains(languageNames(r), "Ruby") {
t.Fatalf("languages = %v, want Ruby", languageNames(r))
}
want := name != "plain Ruby" && name != "ordinary Rakefile"
if got := slices.Contains(packageManagerNames(r), "RubyGems"); got != want {
t.Errorf("package managers = %v, want RubyGems presence %v", packageManagerNames(r), want)
}
if slices.Contains(packageManagerNames(r), "Bundler") {
t.Error("detected Bundler without a Bundler manifest")
}
})
}
}

func TestBundlerManifestLayouts(t *testing.T) {
t.Setenv("PATH", "")
const gemfile = "source 'https://rubygems.org'\ngem 'rubocop'\n"
const gemsRB = "source 'https://rubygems.org'\ngem 'rake'\n"
const oldLock = "GEM\n remote: https://rubygems.org/\n specs:\n rubocop (1.0.0)\n\nDEPENDENCIES\n rubocop\n"
const newLock = "GEM\n remote: https://rubygems.org/\n specs:\n rake (13.0.0)\n\nDEPENDENCIES\n rake\n"
for _, tt := range []struct {
name string
files map[string]string
manifest string
lockfile string
dependency string
}{
{"Gemfile", map[string]string{"Gemfile": gemfile, "Gemfile.lock": oldLock}, "Gemfile", "Gemfile.lock", "rubocop"},
{"gems.rb", map[string]string{"gems.rb": gemsRB, "gems.locked": newLock}, "gems.rb", "gems.locked", "rake"},
{"both layouts", map[string]string{"Gemfile": gemfile, "Gemfile.lock": oldLock, "gems.rb": gemsRB, "gems.locked": newLock}, "gems.rb", "gems.locked", "rake"},
{"missing alternate lockfile", map[string]string{"Gemfile": gemfile, "Gemfile.lock": oldLock, "gems.rb": gemsRB}, "gems.rb", "", "rake"},
{"orphan alternate lockfile", map[string]string{"Gemfile": gemfile, "gems.locked": newLock}, "Gemfile", "", "rubocop"},
} {
t.Run(tt.name, func(t *testing.T) {
dir := t.TempDir()
for path, content := range tt.files {
writeProjectFile(t, dir, path, content)
}
r := runOn(t, dir)
if len(r.PackageManagers) != 1 || r.PackageManagers[0].Name != "Bundler" {
t.Fatalf("package managers = %v, want Bundler", packageManagerNames(r))
}
bundler := r.PackageManagers[0]
if bundler.Lockfile != tt.lockfile {
t.Errorf("lockfile = %q, want %q", bundler.Lockfile, tt.lockfile)
}
for _, path := range []string{tt.manifest, tt.lockfile} {
if path != "" && !slices.Contains(bundler.ConfigFiles, path) {
t.Errorf("config files = %v, want %s", bundler.ConfigFiles, path)
}
}
checkBundlerManifests(t, r, tt.manifest, tt.lockfile, tt.dependency)
})
}
}

func checkBundlerManifests(t *testing.T, r *brief.Report, manifest, lockfile, dependency string) {
t.Helper()
want := []string{manifest}
if lockfile != "" {
want = append(want, lockfile)
}
var got []string
for _, m := range r.Manifests {
got = append(got, m.Path)
}
slices.Sort(got)
slices.Sort(want)
if !slices.Equal(got, want) {
t.Errorf("manifests = %v, want %v", got, want)
}
if len(r.Dependencies) == 0 {
t.Fatal("expected parsed dependencies")
}
for _, dep := range r.Dependencies {
if dep.Name != dependency {
t.Errorf("dependency = %s, want only %s", dep.Name, dependency)
}
}
}

func TestRubyGemsAndBundler(t *testing.T) {
t.Setenv("PATH", "")
dir := t.TempDir()
writeProjectFile(t, dir, "Gemfile", "source 'https://rubygems.org'\ngemspec\n")
writeProjectFile(t, dir, "parser.gemspec", "Gem::Specification.new do |spec|\n spec.name = 'parser'\nend\n")
got := packageManagerNames(runOn(t, dir))
if len(got) != 2 || !slices.Contains(got, "Bundler") || !slices.Contains(got, "RubyGems") {
t.Errorf("package managers = %v, want Bundler and RubyGems", got)
}
}
2 changes: 2 additions & 0 deletions knowledge/_shared/_manifests.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ files = [
".gitmodules",
"Gemfile",
"Gemfile.lock",
"gems.rb",
"gems.locked",
"package.json",
"package-lock.json",
"yarn.lock",
Expand Down
6 changes: 3 additions & 3 deletions knowledge/ruby/bundler.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,19 @@ name = "Bundler"
category = "package_manager"
homepage = "https://bundler.io"
docs = "https://bundler.io/docs.html"
repo = "https://github.com/rubygems/rubygems"
repo = "https://github.com/ruby/rubygems"
description = "Ruby dependency manager"

[detect]
files = ["Gemfile"]
files = ["Gemfile", "gems.rb"]
ecosystems = ["ruby"]

[commands]
run = "bundle install"
alternatives = ["bundle"]

[config]
files = ["Gemfile", "Gemfile.lock", ".bundle/config"]
files = ["Gemfile", "Gemfile.lock", "gems.rb", "gems.locked", ".bundle/config"]
lockfile = "Gemfile.lock"

[taxonomy]
Expand Down
2 changes: 1 addition & 1 deletion knowledge/ruby/language.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ repo = "https://github.com/ruby/ruby"
description = "Dynamic, object-oriented programming language"

[detect]
files = ["Gemfile", "Rakefile", "*.rb", "**/*.rb", "*.gemspec"]
files = ["Gemfile", "gems.rb", "Rakefile", "rakefile", "*.rb", "**/*.rb", "*.gemspec", "**/*.gemspec"]
ecosystems = ["ruby"]

[taxonomy]
Expand Down
24 changes: 24 additions & 0 deletions knowledge/ruby/rubygems.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
[tool]
name = "RubyGems"
category = "package_manager"
homepage = "https://rubygems.org"
docs = "https://guides.rubygems.org/command-reference/"
repo = "https://github.com/ruby/rubygems"
description = "Ruby package manager"

[detect]
files = ["*.gemspec", "**/*.gemspec"]
dependencies = ["hoe"]
ecosystems = ["ruby"]

[detect.file_contains]
"Rakefile" = ["Hoe.spec", "Hoe.new", "Gem::PackageTask"]
"rakefile" = ["Hoe.spec", "Hoe.new", "Gem::PackageTask"]
"Rakefile.rb" = ["Hoe.spec", "Hoe.new", "Gem::PackageTask"]
"rakefile.rb" = ["Hoe.spec", "Hoe.new", "Gem::PackageTask"]

[config]
files = ["*.gemspec", "**/*.gemspec", ".gemrc"]

[taxonomy]
role = ["package-manager"]
Loading