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")
+       }
+}

Reply via email to