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 }