github.com/anchore/syft@v1.4.2-0.20240516191711-1bec1fc5d397/syft/pkg/cataloger/internal/pkgtest/observing_resolver.go (about) 1 package pkgtest 2 3 import ( 4 "context" 5 "fmt" 6 "io" 7 "sort" 8 9 "github.com/scylladb/go-set/strset" 10 11 "github.com/anchore/syft/syft/file" 12 ) 13 14 var _ file.Resolver = (*ObservingResolver)(nil) 15 16 type ObservingResolver struct { 17 decorated file.Resolver 18 pathQueries map[string][]string 19 pathResponses []file.Location 20 contentQueries []file.Location 21 emptyPathResponses map[string][]string 22 } 23 24 func NewObservingResolver(resolver file.Resolver) *ObservingResolver { 25 return &ObservingResolver{ 26 decorated: resolver, 27 pathResponses: make([]file.Location, 0), 28 emptyPathResponses: make(map[string][]string), 29 pathQueries: make(map[string][]string), 30 } 31 } 32 33 // testing helpers... 34 35 func (r *ObservingResolver) ObservedPathQuery(input string) bool { 36 for _, vs := range r.pathQueries { 37 for _, v := range vs { 38 if v == input { 39 return true 40 } 41 } 42 } 43 return false 44 } 45 46 func (r *ObservingResolver) ObservedPathResponses(path string) bool { 47 for _, loc := range r.pathResponses { 48 if loc.RealPath == path { 49 return true 50 } 51 } 52 return false 53 } 54 55 func (r *ObservingResolver) ObservedContentQueries(path string) bool { 56 for _, loc := range r.contentQueries { 57 if loc.RealPath == path { 58 return true 59 } 60 } 61 return false 62 } 63 64 func (r *ObservingResolver) AllContentQueries() []string { 65 observed := strset.New() 66 for _, loc := range r.contentQueries { 67 observed.Add(loc.RealPath) 68 } 69 return observed.List() 70 } 71 72 func (r *ObservingResolver) AllPathQueries() map[string][]string { 73 return r.pathQueries 74 } 75 76 func (r *ObservingResolver) PruneUnfulfilledPathResponses(ignore map[string][]string, ignorePaths ...string) { 77 if ignore == nil { 78 return 79 } 80 // remove any paths that were ignored for specific calls 81 for k, v := range ignore { 82 results := r.emptyPathResponses[k] 83 for _, ig := range v { 84 for i, result := range results { 85 if result == ig { 86 results = append(results[:i], results[i+1:]...) 87 break 88 } 89 } 90 } 91 if len(results) > 0 { 92 r.emptyPathResponses[k] = results 93 } else { 94 delete(r.emptyPathResponses, k) 95 } 96 } 97 98 // remove any paths that were ignored for all calls 99 for _, ig := range ignorePaths { 100 for k, v := range r.emptyPathResponses { 101 for i, result := range v { 102 if result == ig { 103 v = append(v[:i], v[i+1:]...) 104 break 105 } 106 } 107 if len(v) > 0 { 108 r.emptyPathResponses[k] = v 109 } else { 110 delete(r.emptyPathResponses, k) 111 } 112 } 113 } 114 } 115 116 func (r *ObservingResolver) HasUnfulfilledPathRequests() bool { 117 return len(r.emptyPathResponses) > 0 118 } 119 120 func (r *ObservingResolver) PrettyUnfulfilledPathRequests() string { 121 var res string 122 var keys []string 123 124 for k := range r.emptyPathResponses { 125 keys = append(keys, k) 126 } 127 128 sort.Strings(keys) 129 130 for _, k := range keys { 131 res += fmt.Sprintf(" %s: %+v\n", k, r.emptyPathResponses[k]) 132 } 133 return res 134 } 135 136 // For the file path resolver... 137 138 func (r *ObservingResolver) addPathQuery(name string, input ...string) { 139 r.pathQueries[name] = append(r.pathQueries[name], input...) 140 } 141 142 func (r *ObservingResolver) addPathResponse(locs ...file.Location) { 143 r.pathResponses = append(r.pathResponses, locs...) 144 } 145 146 func (r *ObservingResolver) addEmptyPathResponse(name string, locs []file.Location, paths ...string) { 147 if len(locs) == 0 { 148 results := r.emptyPathResponses[name] 149 results = append(results, paths...) 150 r.emptyPathResponses[name] = results 151 } 152 } 153 154 func (r *ObservingResolver) FilesByPath(paths ...string) ([]file.Location, error) { 155 name := "FilesByPath" 156 r.addPathQuery(name, paths...) 157 158 locs, err := r.decorated.FilesByPath(paths...) 159 160 r.addPathResponse(locs...) 161 r.addEmptyPathResponse(name, locs, paths...) 162 return locs, err 163 } 164 165 func (r *ObservingResolver) FilesByGlob(patterns ...string) ([]file.Location, error) { 166 name := "FilesByGlob" 167 r.addPathQuery(name, patterns...) 168 169 locs, err := r.decorated.FilesByGlob(patterns...) 170 171 r.addPathResponse(locs...) 172 r.addEmptyPathResponse(name, locs, patterns...) 173 return locs, err 174 } 175 176 func (r *ObservingResolver) FilesByMIMEType(types ...string) ([]file.Location, error) { 177 name := "FilesByMIMEType" 178 r.addPathQuery(name, types...) 179 180 locs, err := r.decorated.FilesByMIMEType(types...) 181 182 r.addPathResponse(locs...) 183 r.addEmptyPathResponse(name, locs, types...) 184 return locs, err 185 } 186 187 func (r *ObservingResolver) RelativeFileByPath(l file.Location, path string) *file.Location { 188 name := "RelativeFileByPath" 189 r.addPathQuery(name, path) 190 191 loc := r.decorated.RelativeFileByPath(l, path) 192 193 if loc != nil { 194 r.addPathResponse(*loc) 195 } else { 196 results := r.emptyPathResponses[name] 197 results = append(results, path) 198 r.emptyPathResponses[name] = results 199 } 200 return loc 201 } 202 203 // For the content resolver methods... 204 205 func (r *ObservingResolver) FileContentsByLocation(location file.Location) (io.ReadCloser, error) { 206 r.contentQueries = append(r.contentQueries, location) 207 reader, err := r.decorated.FileContentsByLocation(location) 208 return reader, err 209 } 210 211 // For the remaining resolver methods... 212 213 func (r *ObservingResolver) AllLocations(ctx context.Context) <-chan file.Location { 214 return r.decorated.AllLocations(ctx) 215 } 216 217 func (r *ObservingResolver) HasPath(s string) bool { 218 return r.decorated.HasPath(s) 219 } 220 221 func (r *ObservingResolver) FileMetadataByLocation(location file.Location) (file.Metadata, error) { 222 return r.decorated.FileMetadataByLocation(location) 223 }