github.com/google/osv-scalibr@v0.4.1/extractor/filesystem/language/rust/cargoauditable/cargoauditable_test.go (about)

     1  // Copyright 2025 Google LLC
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //      http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package cargoauditable_test
    16  
    17  import (
    18  	"errors"
    19  	"io/fs"
    20  	"os"
    21  	"path/filepath"
    22  	"testing"
    23  
    24  	"github.com/google/go-cmp/cmp"
    25  	"github.com/google/go-cmp/cmp/cmpopts"
    26  	"github.com/google/osv-scalibr/extractor"
    27  	"github.com/google/osv-scalibr/extractor/filesystem"
    28  	"github.com/google/osv-scalibr/extractor/filesystem/language/rust/cargoauditable"
    29  	"github.com/google/osv-scalibr/extractor/filesystem/simplefileapi"
    30  	scalibrfs "github.com/google/osv-scalibr/fs"
    31  	"github.com/google/osv-scalibr/inventory"
    32  	"github.com/google/osv-scalibr/purl"
    33  	"github.com/google/osv-scalibr/stats"
    34  	"github.com/google/osv-scalibr/testing/fakefs"
    35  	"github.com/google/osv-scalibr/testing/testcollector"
    36  )
    37  
    38  func TestFileRequired(t *testing.T) {
    39  	tests := []struct {
    40  		name             string
    41  		path             string
    42  		mode             fs.FileMode
    43  		fileSizeBytes    int64
    44  		maxFileSizeBytes int64
    45  		wantRequired     bool
    46  		wantResultMetric stats.FileRequiredResult
    47  	}{
    48  		{
    49  			name:             "executable required if size less than maxFileSizeBytes",
    50  			path:             "some/path/a",
    51  			mode:             0766,
    52  			fileSizeBytes:    100,
    53  			maxFileSizeBytes: 1000,
    54  			wantRequired:     true,
    55  			wantResultMetric: stats.FileRequiredResultOK,
    56  		},
    57  		{
    58  			name:             "executable required if size equal to maxFileSizeBytes",
    59  			path:             "some/path/a",
    60  			mode:             0766,
    61  			fileSizeBytes:    1000,
    62  			maxFileSizeBytes: 1000,
    63  			wantRequired:     true,
    64  			wantResultMetric: stats.FileRequiredResultOK,
    65  		},
    66  		{
    67  			name:             "executable not required if size greater than maxFileSizeBytes",
    68  			path:             "some/path/a",
    69  			mode:             0766,
    70  			fileSizeBytes:    1000,
    71  			maxFileSizeBytes: 100,
    72  			wantRequired:     false,
    73  			wantResultMetric: stats.FileRequiredResultSizeLimitExceeded,
    74  		},
    75  		{
    76  			name:             "executable required if maxFileSizeBytes explicitly set to 0",
    77  			path:             "some/path/a",
    78  			mode:             0766,
    79  			fileSizeBytes:    1000,
    80  			maxFileSizeBytes: 0,
    81  			wantRequired:     true,
    82  			wantResultMetric: stats.FileRequiredResultOK,
    83  		},
    84  	}
    85  
    86  	for _, tt := range tests {
    87  		t.Run(tt.name, func(t *testing.T) {
    88  			collector := testcollector.New()
    89  			e := cargoauditable.New(cargoauditable.Config{
    90  				Stats:            collector,
    91  				MaxFileSizeBytes: tt.maxFileSizeBytes,
    92  			})
    93  
    94  			// Set a default file size if not specified.
    95  			fileSizeBytes := tt.fileSizeBytes
    96  			if fileSizeBytes == 0 {
    97  				fileSizeBytes = 1000
    98  			}
    99  
   100  			if got := e.FileRequired(simplefileapi.New(tt.path, fakefs.FakeFileInfo{
   101  				FileName: filepath.Base(tt.path),
   102  				FileMode: tt.mode,
   103  				FileSize: fileSizeBytes,
   104  			})); got != tt.wantRequired {
   105  				t.Fatalf("FileRequired(%s): got %v, want %v", tt.path, got, tt.wantRequired)
   106  			}
   107  
   108  			gotResultMetric := collector.FileRequiredResult(tt.path)
   109  			if gotResultMetric != tt.wantResultMetric {
   110  				t.Errorf("FileRequired(%s) recorded result metric %v, want result metric %v", tt.path, gotResultMetric, tt.wantResultMetric)
   111  			}
   112  		})
   113  	}
   114  }
   115  
   116  func TestExtract(t *testing.T) {
   117  	tests := []struct {
   118  		name             string
   119  		path             string
   120  		wantPackages     []*extractor.Package
   121  		wantErr          error
   122  		wantResultMetric stats.FileExtractedResult
   123  	}{
   124  		{
   125  			name: "uses_serde_json",
   126  			path: "testdata/uses_serde_json/uses_serde_json",
   127  			wantPackages: []*extractor.Package{
   128  				{
   129  					Name:      "itoa",
   130  					Version:   "1.0.14",
   131  					PURLType:  purl.TypeCargo,
   132  					Locations: []string{"testdata/uses_serde_json/uses_serde_json"},
   133  				},
   134  				{
   135  					Name:      "memchr",
   136  					Version:   "2.7.4",
   137  					PURLType:  purl.TypeCargo,
   138  					Locations: []string{"testdata/uses_serde_json/uses_serde_json"},
   139  				},
   140  				{
   141  					Name:      "proc-macro2",
   142  					Version:   "1.0.92",
   143  					PURLType:  purl.TypeCargo,
   144  					Locations: []string{"testdata/uses_serde_json/uses_serde_json"},
   145  				},
   146  				{
   147  					Name:      "quote",
   148  					Version:   "1.0.38",
   149  					PURLType:  purl.TypeCargo,
   150  					Locations: []string{"testdata/uses_serde_json/uses_serde_json"},
   151  				},
   152  				{
   153  					Name:      "ryu",
   154  					Version:   "1.0.18",
   155  					PURLType:  purl.TypeCargo,
   156  					Locations: []string{"testdata/uses_serde_json/uses_serde_json"},
   157  				},
   158  				{
   159  					Name:      "serde",
   160  					Version:   "1.0.217",
   161  					PURLType:  purl.TypeCargo,
   162  					Locations: []string{"testdata/uses_serde_json/uses_serde_json"},
   163  				},
   164  				{
   165  					Name:      "serde_derive",
   166  					Version:   "1.0.217",
   167  					PURLType:  purl.TypeCargo,
   168  					Locations: []string{"testdata/uses_serde_json/uses_serde_json"},
   169  				},
   170  				{
   171  					Name:      "serde_json",
   172  					Version:   "1.0.135",
   173  					PURLType:  purl.TypeCargo,
   174  					Locations: []string{"testdata/uses_serde_json/uses_serde_json"},
   175  				},
   176  				{
   177  					Name:      "syn",
   178  					Version:   "2.0.95",
   179  					PURLType:  purl.TypeCargo,
   180  					Locations: []string{"testdata/uses_serde_json/uses_serde_json"},
   181  				},
   182  				{
   183  					Name:      "unicode-ident",
   184  					Version:   "1.0.14",
   185  					PURLType:  purl.TypeCargo,
   186  					Locations: []string{"testdata/uses_serde_json/uses_serde_json"},
   187  				},
   188  				{
   189  					Name:      "uses_json",
   190  					Version:   "0.1.0",
   191  					PURLType:  purl.TypeCargo,
   192  					Locations: []string{"testdata/uses_serde_json/uses_serde_json"},
   193  				},
   194  			},
   195  		},
   196  		{
   197  			name: "no_deps",
   198  			path: "testdata/no_deps/no_deps",
   199  			wantPackages: []*extractor.Package{
   200  				{
   201  					Name:      "no_deps",
   202  					Version:   "0.1.0",
   203  					PURLType:  purl.TypeCargo,
   204  					Locations: []string{"testdata/no_deps/no_deps"},
   205  				},
   206  			},
   207  		},
   208  		{
   209  			name:             "not_binary",
   210  			path:             "testdata/not_binary/not_binary",
   211  			wantPackages:     nil,
   212  			wantResultMetric: stats.FileExtractedResultErrorUnknown,
   213  		},
   214  	}
   215  
   216  	for _, tt := range tests {
   217  		t.Run(tt.name, func(t *testing.T) {
   218  			f, err := os.Open(tt.path)
   219  			if err != nil {
   220  				t.Fatalf("os.Open(%s) unexpected error: %v", tt.path, err)
   221  			}
   222  			defer f.Close()
   223  
   224  			info, err := f.Stat()
   225  			if err != nil {
   226  				t.Fatalf("f.Stat() for %q unexpected error: %v", tt.path, err)
   227  			}
   228  
   229  			collector := testcollector.New()
   230  
   231  			input := &filesystem.ScanInput{FS: scalibrfs.DirFS("."), Path: tt.path, Info: info, Reader: f}
   232  
   233  			e := cargoauditable.New(cargoauditable.Config{Stats: collector})
   234  			got, err := e.Extract(t.Context(), input)
   235  			if !errors.Is(err, tt.wantErr) {
   236  				t.Fatalf("Extract(%s) got error: %v, want error: %v", tt.path, err, tt.wantErr)
   237  			}
   238  			sort := func(a, b *extractor.Package) bool { return a.Name < b.Name }
   239  			wantInv := inventory.Inventory{Packages: tt.wantPackages}
   240  			if diff := cmp.Diff(wantInv, got, cmpopts.SortSlices(sort)); diff != "" {
   241  				t.Fatalf("Extract(%s) (-want +got):\n%s", tt.path, diff)
   242  			}
   243  
   244  			wantResultMetric := tt.wantResultMetric
   245  			if wantResultMetric == "" && tt.wantErr == nil {
   246  				wantResultMetric = stats.FileExtractedResultSuccess
   247  			}
   248  			gotResultMetric := collector.FileExtractedResult(tt.path)
   249  			if gotResultMetric != wantResultMetric {
   250  				t.Errorf("Extract(%s) recorded result metric %v, want result metric %v", tt.path, gotResultMetric, wantResultMetric)
   251  			}
   252  
   253  			gotFileSizeMetric := collector.FileExtractedFileSize(tt.path)
   254  			if gotFileSizeMetric != info.Size() {
   255  				t.Errorf("Extract(%s) recorded file size %v, want file size %v", tt.path, gotFileSizeMetric, info.Size())
   256  			}
   257  		})
   258  	}
   259  }