github.com/google/osv-scalibr@v0.4.1/plugin/plugin_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 plugin_test
    16  
    17  import (
    18  	"testing"
    19  
    20  	"github.com/google/go-cmp/cmp"
    21  	"github.com/google/go-cmp/cmp/cmpopts"
    22  	"github.com/google/osv-scalibr/extractor/filesystem/os/homebrew"
    23  	"github.com/google/osv-scalibr/extractor/filesystem/os/snap"
    24  	"github.com/google/osv-scalibr/plugin"
    25  )
    26  
    27  type fakePlugin struct {
    28  	reqs *plugin.Capabilities
    29  }
    30  
    31  func (fakePlugin) Name() string                         { return "fake-plugin" }
    32  func (fakePlugin) Version() int                         { return 0 }
    33  func (p fakePlugin) Requirements() *plugin.Capabilities { return p.reqs }
    34  
    35  func TestValidateRequirements(t *testing.T) {
    36  	testCases := []struct {
    37  		desc       string
    38  		pluginReqs *plugin.Capabilities
    39  		capabs     *plugin.Capabilities
    40  		wantErr    error
    41  	}{
    42  		{
    43  			desc:       "No requirements",
    44  			pluginReqs: &plugin.Capabilities{},
    45  			capabs:     &plugin.Capabilities{},
    46  			wantErr:    nil,
    47  		},
    48  		{
    49  			desc:       "All requirements satisfied",
    50  			pluginReqs: &plugin.Capabilities{Network: plugin.NetworkOnline, DirectFS: true},
    51  			capabs:     &plugin.Capabilities{Network: plugin.NetworkOnline, DirectFS: true},
    52  			wantErr:    nil,
    53  		},
    54  		{
    55  			desc:       "One requirement not satisfied",
    56  			pluginReqs: &plugin.Capabilities{Network: plugin.NetworkOnline, DirectFS: true},
    57  			capabs:     &plugin.Capabilities{Network: plugin.NetworkOnline, DirectFS: false},
    58  			wantErr:    cmpopts.AnyError,
    59  		},
    60  		{
    61  			desc:       "No requirement satisfied",
    62  			pluginReqs: &plugin.Capabilities{Network: plugin.NetworkOnline, DirectFS: true},
    63  			capabs:     &plugin.Capabilities{Network: plugin.NetworkOffline, DirectFS: false},
    64  			wantErr:    cmpopts.AnyError,
    65  		},
    66  		{
    67  			desc:       "Any network 1",
    68  			pluginReqs: &plugin.Capabilities{Network: plugin.NetworkAny},
    69  			capabs:     &plugin.Capabilities{Network: plugin.NetworkOffline},
    70  			wantErr:    nil,
    71  		},
    72  		{
    73  			desc:       "Any network 2",
    74  			pluginReqs: &plugin.Capabilities{Network: plugin.NetworkAny},
    75  			capabs:     &plugin.Capabilities{Network: plugin.NetworkOnline},
    76  			wantErr:    nil,
    77  		},
    78  		{
    79  			desc:       "Wrong OS",
    80  			pluginReqs: &plugin.Capabilities{OS: plugin.OSLinux},
    81  			capabs:     &plugin.Capabilities{OS: plugin.OSWindows},
    82  			wantErr:    cmpopts.AnyError,
    83  		},
    84  		{
    85  			desc:       "Unix OS not satisfied",
    86  			pluginReqs: &plugin.Capabilities{OS: plugin.OSUnix},
    87  			capabs:     &plugin.Capabilities{OS: plugin.OSWindows},
    88  			wantErr:    cmpopts.AnyError,
    89  		},
    90  		{
    91  			desc:       "Unix OS satisfied",
    92  			pluginReqs: &plugin.Capabilities{OS: plugin.OSUnix},
    93  			capabs:     &plugin.Capabilities{OS: plugin.OSMac},
    94  			wantErr:    nil,
    95  		},
    96  	}
    97  
    98  	for _, tc := range testCases {
    99  		t.Run(tc.desc, func(t *testing.T) {
   100  			p := fakePlugin{reqs: tc.pluginReqs}
   101  			err := plugin.ValidateRequirements(p, tc.capabs)
   102  			if !cmp.Equal(err, tc.wantErr, cmpopts.EquateErrors()) {
   103  				t.Fatalf("plugin.ValidateRequirements(%v, %v) got error: %v, want: %v\n", tc.pluginReqs, tc.capabs, err, tc.wantErr)
   104  			}
   105  		})
   106  	}
   107  }
   108  
   109  func TestFilterByCapabilities(t *testing.T) {
   110  	capab := &plugin.Capabilities{OS: plugin.OSLinux}
   111  	pls := []plugin.Plugin{snap.NewDefault(), homebrew.New()}
   112  	got := plugin.FilterByCapabilities(pls, capab)
   113  	if len(got) != 1 {
   114  		t.Fatalf("plugin.FilterCapabilities(%v, %v): want 1 plugin, got %d", pls, capab, len(got))
   115  	}
   116  	gotName := got[0].Name()
   117  	wantName := "os/snap" // os/homebrew is for Mac only
   118  	if gotName != wantName {
   119  		t.Fatalf("plugin.FilterCapabilities(%v, %v): want plugin %q, got %q", pls, capab, wantName, gotName)
   120  	}
   121  }
   122  
   123  func TestString(t *testing.T) {
   124  	testCases := []struct {
   125  		desc string
   126  		s    *plugin.ScanStatus
   127  		want string
   128  	}{
   129  		{
   130  			desc: "Successful_scan",
   131  			s:    &plugin.ScanStatus{Status: plugin.ScanStatusSucceeded},
   132  			want: "SUCCEEDED",
   133  		},
   134  		{
   135  			desc: "Partially_successful_scan",
   136  			s:    &plugin.ScanStatus{Status: plugin.ScanStatusPartiallySucceeded},
   137  			want: "PARTIALLY_SUCCEEDED",
   138  		},
   139  		{
   140  			desc: "Failed_scan",
   141  			s:    &plugin.ScanStatus{Status: plugin.ScanStatusFailed, FailureReason: "failure"},
   142  			want: "FAILED: failure",
   143  		},
   144  		{
   145  			desc: "Unspecified_status",
   146  			s:    &plugin.ScanStatus{},
   147  			want: "UNSPECIFIED",
   148  		},
   149  	}
   150  
   151  	for _, tc := range testCases {
   152  		t.Run(tc.desc, func(t *testing.T) {
   153  			got := tc.s.String()
   154  			if got != tc.want {
   155  				t.Errorf("%v.String(): Got %s, want %s", tc.s, got, tc.want)
   156  			}
   157  		})
   158  	}
   159  }
   160  
   161  func TestDedupeStatuses(t *testing.T) {
   162  	testCases := []struct {
   163  		desc string
   164  		s    []*plugin.Status
   165  		want []*plugin.Status
   166  	}{
   167  		{
   168  			desc: "Separate_plugins",
   169  			s: []*plugin.Status{
   170  				{
   171  					Name:   "plugin1",
   172  					Status: &plugin.ScanStatus{Status: plugin.ScanStatusSucceeded},
   173  				},
   174  				{
   175  					Name:   "plugin2",
   176  					Status: &plugin.ScanStatus{Status: plugin.ScanStatusSucceeded},
   177  				},
   178  			},
   179  			want: []*plugin.Status{
   180  				{
   181  					Name:   "plugin1",
   182  					Status: &plugin.ScanStatus{Status: plugin.ScanStatusSucceeded},
   183  				},
   184  				{
   185  					Name:   "plugin2",
   186  					Status: &plugin.ScanStatus{Status: plugin.ScanStatusSucceeded},
   187  				},
   188  			},
   189  		},
   190  		{
   191  			desc: "Both_successful",
   192  			s: []*plugin.Status{
   193  				{
   194  					Name:   "plugin1",
   195  					Status: &plugin.ScanStatus{Status: plugin.ScanStatusSucceeded},
   196  				},
   197  				{
   198  					Name:   "plugin1",
   199  					Status: &plugin.ScanStatus{Status: plugin.ScanStatusSucceeded},
   200  				},
   201  			},
   202  			want: []*plugin.Status{
   203  				{
   204  					Name:   "plugin1",
   205  					Status: &plugin.ScanStatus{Status: plugin.ScanStatusSucceeded},
   206  				},
   207  			},
   208  		},
   209  		{
   210  			desc: "One_success_one_partial_success",
   211  			s: []*plugin.Status{
   212  				{
   213  					Name:   "plugin1",
   214  					Status: &plugin.ScanStatus{Status: plugin.ScanStatusSucceeded},
   215  				},
   216  				{
   217  					Name: "plugin1",
   218  					Status: &plugin.ScanStatus{
   219  						Status:        plugin.ScanStatusPartiallySucceeded,
   220  						FailureReason: "reason",
   221  					},
   222  				},
   223  			},
   224  			want: []*plugin.Status{
   225  				{
   226  					Name: "plugin1",
   227  					Status: &plugin.ScanStatus{
   228  						Status:        plugin.ScanStatusPartiallySucceeded,
   229  						FailureReason: "reason",
   230  					},
   231  				},
   232  			},
   233  		},
   234  		{
   235  			desc: "One_success_one_failure",
   236  			s: []*plugin.Status{
   237  				{
   238  					Name:   "plugin1",
   239  					Status: &plugin.ScanStatus{Status: plugin.ScanStatusSucceeded},
   240  				},
   241  				{
   242  					Name: "plugin1",
   243  					Status: &plugin.ScanStatus{
   244  						Status:        plugin.ScanStatusFailed,
   245  						FailureReason: "reason",
   246  					},
   247  				},
   248  			},
   249  			want: []*plugin.Status{
   250  				{
   251  					Name: "plugin1",
   252  					Status: &plugin.ScanStatus{
   253  						Status:        plugin.ScanStatusFailed,
   254  						FailureReason: "reason",
   255  					},
   256  				},
   257  			},
   258  		},
   259  		{
   260  			desc: "One_partial_success_one_failure",
   261  			s: []*plugin.Status{
   262  				{
   263  					Name: "plugin1",
   264  					Status: &plugin.ScanStatus{
   265  						Status:        plugin.ScanStatusPartiallySucceeded,
   266  						FailureReason: "reason1",
   267  					},
   268  				},
   269  				{
   270  					Name: "plugin1",
   271  					Status: &plugin.ScanStatus{
   272  						Status:        plugin.ScanStatusFailed,
   273  						FailureReason: "reason2",
   274  					},
   275  				},
   276  			},
   277  			want: []*plugin.Status{
   278  				{
   279  					Name: "plugin1",
   280  					Status: &plugin.ScanStatus{
   281  						Status:        plugin.ScanStatusFailed,
   282  						FailureReason: "reason1\nreason2",
   283  					},
   284  				},
   285  			},
   286  		},
   287  		{
   288  			desc: "File_errors_combined",
   289  			s: []*plugin.Status{
   290  				{
   291  					Name: "plugin1",
   292  					Status: &plugin.ScanStatus{
   293  						Status:        plugin.ScanStatusFailed,
   294  						FailureReason: "encountered 1 error(s) while running plugin; check file-specific errors for details",
   295  						FileErrors: []*plugin.FileError{
   296  							{FilePath: "file1", ErrorMessage: "msg1"},
   297  						},
   298  					},
   299  				},
   300  				{
   301  					Name: "plugin1",
   302  					Status: &plugin.ScanStatus{
   303  						Status:        plugin.ScanStatusFailed,
   304  						FailureReason: "encountered 1 error(s) while running plugin; check file-specific errors for details",
   305  						FileErrors: []*plugin.FileError{
   306  							{FilePath: "file2", ErrorMessage: "msg2"},
   307  						},
   308  					},
   309  				},
   310  			},
   311  			want: []*plugin.Status{
   312  				{
   313  					Name: "plugin1",
   314  					Status: &plugin.ScanStatus{
   315  						Status:        plugin.ScanStatusFailed,
   316  						FailureReason: "encountered 2 error(s) while running plugin; check file-specific errors for details",
   317  						FileErrors: []*plugin.FileError{
   318  							{FilePath: "file1", ErrorMessage: "msg1"},
   319  							{FilePath: "file2", ErrorMessage: "msg2"},
   320  						},
   321  					},
   322  				},
   323  			},
   324  		},
   325  	}
   326  
   327  	for _, tc := range testCases {
   328  		t.Run(tc.desc, func(t *testing.T) {
   329  			got := plugin.DedupeStatuses(tc.s)
   330  			sort := func(a, b *plugin.Status) bool { return a.Name < b.Name }
   331  			if diff := cmp.Diff(tc.want, got, cmpopts.SortSlices(sort)); diff != "" {
   332  				t.Fatalf("plugin.DedupeStatuses(%v) (-want +got):\n%s", tc.s, diff)
   333  			}
   334  		})
   335  	}
   336  }