This is an automated email from the ASF dual-hosted git repository.
kezhenxu94 pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/skywalking-eyes.git
The following commit(s) were added to refs/heads/main by this push:
new 5a19c65 Fixed a panic caused by nil dependency specs in Ruby
Gemfile.lock resolver (#207)
5a19c65 is described below
commit 5a19c655ba4cb6befa50c1bc4c1f03b6a68ce871
Author: |7eter l-|. l3oling <[email protected]>
AuthorDate: Sat Sep 13 14:48:46 2025 +0700
Fixed a panic caused by nil dependency specs in Ruby Gemfile.lock resolver
(#207)
- now treats missing specs as unresolved licenses
- adds them to the invalid list
- License fetch logic handles empty gem versions gracefully
- tests updated
- handle missing specs gracefully with mocked HTTP responses
- imports and packages were refactored to enable mocking and improve test
structure
---
pkg/deps/ruby.go | 12 +++++++-
pkg/deps/ruby_test.go | 85 ++++++++++++++++++++++++++++++++++++++++++++++-----
2 files changed, 88 insertions(+), 9 deletions(-)
diff --git a/pkg/deps/ruby.go b/pkg/deps/ruby.go
index 5bbe5a5..2cebd3b 100644
--- a/pkg/deps/ruby.go
+++ b/pkg/deps/ruby.go
@@ -92,7 +92,11 @@ func (r *GemfileLockResolver) Resolve(lockfile string,
config *ConfigDeps, repor
// Resolve licenses for included gems
for name := range include {
- version := specs[name].Version
+ // Some roots may not exist in the specs graph (e.g.,
git-sourced gems)
+ var version string
+ if spec, ok := specs[name]; ok && spec != nil {
+ version = spec.Version
+ }
if exclude, _ := config.IsExcluded(name, version); exclude {
continue
}
@@ -103,6 +107,7 @@ func (r *GemfileLockResolver) Resolve(lockfile string,
config *ConfigDeps, repor
licenseID, err := fetchRubyGemsLicense(name, version)
if err != nil || licenseID == "" {
+ // Gracefully treat as unresolved license and record in
report
report.Skip(&Result{Dependency: name, LicenseSpdxID:
Unknown, Version: version})
continue
}
@@ -269,6 +274,11 @@ type rubyGemsVersionInfo struct {
}
func fetchRubyGemsLicense(name, version string) (string, error) {
+ // If version is unknown (e.g., git-sourced), query latest gem info
endpoint
+ if strings.TrimSpace(version) == "" {
+ url := fmt.Sprintf("https://rubygems.org/api/v1/gems/%s.json",
name)
+ return fetchRubyGemsLicenseFrom(url)
+ }
// Prefer version-specific API
url :=
fmt.Sprintf("https://rubygems.org/api/v2/rubygems/%s/versions/%s.json", name,
version)
licenseID, err := fetchRubyGemsLicenseFrom(url)
diff --git a/pkg/deps/ruby_test.go b/pkg/deps/ruby_test.go
index 889a73d..c925cd0 100644
--- a/pkg/deps/ruby_test.go
+++ b/pkg/deps/ruby_test.go
@@ -15,18 +15,18 @@
// specific language governing permissions and limitations
// under the License.
-package deps_test
+package deps
import (
"bufio"
"embed"
+ "io"
"io/fs"
+ "net/http"
"os"
"path/filepath"
"strings"
"testing"
-
- "github.com/apache/skywalking-eyes/pkg/deps"
)
func writeFileRuby(fileName, content string) error {
@@ -73,7 +73,7 @@ func copyRuby(assetDir, destination string) error {
}
func TestRubyGemfileLockResolver(t *testing.T) {
- resolver := new(deps.GemfileLockResolver)
+ resolver := new(GemfileLockResolver)
// App case: include all specs (3)
{
@@ -85,12 +85,12 @@ func TestRubyGemfileLockResolver(t *testing.T) {
if !resolver.CanResolve(lock) {
t.Fatalf("GemfileLockResolver cannot resolve %s", lock)
}
- cfg := &deps.ConfigDeps{Files: []string{lock}, Licenses:
[]*deps.ConfigDepLicense{
+ cfg := &ConfigDeps{Files: []string{lock}, Licenses:
[]*ConfigDepLicense{
{Name: "rake", Version: "13.0.6", License: "MIT"},
{Name: "rspec", Version: "3.10.0", License: "MIT"},
{Name: "rspec-core", Version: "3.10.1", License: "MIT"},
}}
- report := deps.Report{}
+ report := Report{}
if err := resolver.Resolve(lock, cfg, &report); err != nil {
t.Fatal(err)
}
@@ -106,10 +106,10 @@ func TestRubyGemfileLockResolver(t *testing.T) {
t.Fatal(err)
}
lock := filepath.Join(tmp, "Gemfile.lock")
- cfg := &deps.ConfigDeps{Files: []string{lock}, Licenses:
[]*deps.ConfigDepLicense{
+ cfg := &ConfigDeps{Files: []string{lock}, Licenses:
[]*ConfigDepLicense{
{Name: "rake", Version: "13.0.6", License: "MIT"},
}}
- report := deps.Report{}
+ report := Report{}
if err := resolver.Resolve(lock, cfg, &report); err != nil {
t.Fatal(err)
}
@@ -118,3 +118,72 @@ func TestRubyGemfileLockResolver(t *testing.T) {
}
}
}
+
+// mock RoundTripper to control HTTP responses
+type roundTripFunc func(*http.Request) (*http.Response, error)
+
+func (f roundTripFunc) RoundTrip(req *http.Request) (*http.Response, error) {
return f(req) }
+
+func TestRubyMissingSpecIsSkippedGracefully(t *testing.T) {
+ // Mock HTTP client to avoid real network: always return 404 Not Found
+ saved := httpClientRuby
+ httpClientRuby = &http.Client{Transport: roundTripFunc(func(r
*http.Request) (*http.Response, error) {
+ return &http.Response{
+ StatusCode: http.StatusNotFound,
+ Status: "404 Not Found",
+ Body: io.NopCloser(strings.NewReader("{}")),
+ Header: make(http.Header),
+ }, nil
+ })}
+ defer func() { httpClientRuby = saved }()
+
+ // Create a Gemfile.lock where a dependency is not present in specs
+ content := "" +
+ "GEM\n" +
+ " remote: https://rubygems.org/\n" +
+ " specs:\n" +
+ " rake (13.0.6)\n" +
+ "\n" +
+ "PLATFORMS\n" +
+ " ruby\n" +
+ "\n" +
+ "DEPENDENCIES\n" +
+ " rake\n" +
+ " missing_gem\n" +
+ "\n" +
+ "BUNDLED WITH\n" +
+ " 2.4.10\n"
+
+ dir := t.TempDir()
+ lock := filepath.Join(dir, "Gemfile.lock")
+ if err := writeFileRuby(lock, content); err != nil {
+ t.Fatal(err)
+ }
+
+ resolver := new(GemfileLockResolver)
+ cfg := &ConfigDeps{Files: []string{lock}, Licenses: []*ConfigDepLicense{
+ {Name: "rake", Version: "13.0.6", License: "MIT"}, // only rake
is configured; missing_gem should be skipped
+ }}
+ report := Report{}
+ if err := resolver.Resolve(lock, cfg, &report); err != nil {
+ t.Fatal(err)
+ }
+
+ if got := len(report.Resolved) + len(report.Skipped); got != 2 {
+ t.Fatalf("expected 2 dependencies total, got %d", got)
+ }
+
+ // Ensure 'missing_gem' is in skipped with empty version
+ found := false
+ for _, s := range report.Skipped {
+ if s.Dependency == "missing_gem" {
+ found = true
+ if s.Version != "" {
+ t.Fatalf("expected empty version for
missing_gem, got %q", s.Version)
+ }
+ }
+ }
+ if !found {
+ t.Fatalf("expected missing_gem to be marked as skipped")
+ }
+}