| Brian Sheedy | 34355d0 | 2025-03-06 11:34:36 -0800 | [diff] [blame] | 1 | // Copyright 2025 Google LLC |
| 2 | // |
| 3 | // Redistribution and use in source and binary forms, with or without |
| 4 | // modification, are permitted provided that the following conditions are met: |
| 5 | // |
| 6 | // 1. Redistributions of source code must retain the above copyright notice, this |
| 7 | // list of conditions and the following disclaimer. |
| 8 | // |
| 9 | // 2. Redistributions in binary form must reproduce the above copyright notice, |
| 10 | // this list of conditions and the following disclaimer in the documentation |
| 11 | // and/or other materials provided with the distribution. |
| 12 | // |
| 13 | // 3. Neither the name of the copyright holder nor the names of its |
| 14 | // contributors may be used to endorse or promote products derived from |
| 15 | // this software without specific prior written permission. |
| 16 | // |
| 17 | // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" |
| 18 | // AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE |
| 19 | // IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE |
| 20 | // DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE |
| 21 | // FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL |
| 22 | // DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR |
| 23 | // SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER |
| 24 | // CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, |
| 25 | // OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE |
| 26 | // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
| 27 | |
| 28 | package glob |
| 29 | |
| 30 | import ( |
| 31 | "fmt" |
| 32 | "strings" |
| 33 | "testing" |
| 34 | |
| 35 | "dawn.googlesource.com/dawn/tools/src/oswrapper" |
| 36 | "github.com/stretchr/testify/require" |
| 37 | ) |
| 38 | |
| 39 | func TestGlob(t *testing.T) { |
| 40 | tests := []struct { |
| 41 | name string |
| 42 | input string |
| 43 | want []string |
| 44 | wantErr bool |
| 45 | wantErrMsg string |
| 46 | }{ |
| 47 | // We can't really test the error condition from the filepath.Abs() call, as |
| 48 | // it basically only fails if the given path is not absolute and the cwd |
| 49 | // does not exist. |
| 50 | { |
| 51 | name: "Match no wildcard", |
| 52 | input: "/a/1/file_1.txt", |
| 53 | want: []string{"/a/1/file_1.txt"}, |
| 54 | }, |
| 55 | { |
| 56 | name: "No match no wildcard is directory", |
| 57 | input: "/a/1", |
| 58 | want: []string{}, |
| 59 | }, |
| 60 | { |
| 61 | name: "Match star wildcard files", |
| 62 | input: "/a/1/*", |
| 63 | want: []string{"/a/1/file_1.txt", "/a/1/file_2.txt"}, |
| 64 | }, |
| 65 | { |
| 66 | name: "Match star wildcard child directory", |
| 67 | input: "/a/*/file_1.txt", |
| 68 | want: []string{"/a/1/file_1.txt", "/a/2/file_1.txt", "/a/3/file_1.txt"}, |
| 69 | }, |
| 70 | { |
| Brian Sheedy | b13d15c | 2025-11-20 06:56:17 -0800 | [diff] [blame] | 71 | name: "Match star wildcard parent directory", |
| 72 | input: "/*/1/file_1.txt", |
| 73 | want: []string{"/a/1/file_1.txt", "/b/1/file_1.txt", "/c/1/file_1.txt"}, |
| Brian Sheedy | 34355d0 | 2025-03-06 11:34:36 -0800 | [diff] [blame] | 74 | }, |
| 75 | { |
| 76 | name: "Match question wildcard files", |
| 77 | input: "/a/1/file_?.txt", |
| 78 | want: []string{"/a/1/file_1.txt", "/a/1/file_2.txt"}, |
| 79 | }, |
| 80 | { |
| 81 | name: "No match question wildcard files", |
| 82 | input: "/a/1/?file_1.txt", |
| 83 | want: []string{}, |
| 84 | }, |
| 85 | { |
| 86 | name: "Match question filecard child directory", |
| 87 | input: "/a/?/file_1.txt", |
| 88 | want: []string{"/a/1/file_1.txt", "/a/2/file_1.txt", "/a/3/file_1.txt"}, |
| 89 | }, |
| 90 | { |
| Brian Sheedy | b13d15c | 2025-11-20 06:56:17 -0800 | [diff] [blame] | 91 | name: "Match question filecard parent directory", |
| 92 | input: "/?/1/file_1.txt", |
| 93 | want: []string{"/a/1/file_1.txt", "/b/1/file_1.txt", "/c/1/file_1.txt"}, |
| Brian Sheedy | 34355d0 | 2025-03-06 11:34:36 -0800 | [diff] [blame] | 94 | }, |
| 95 | { |
| 96 | name: "No match no wildcard non existent file", |
| 97 | input: "/a/1/file_3.txt", |
| 98 | want: []string{}, |
| 99 | }, |
| 100 | { |
| 101 | name: "No match no wildcard non existent directory", |
| 102 | input: "/a/5/file_1.txt", |
| 103 | want: []string{}, |
| 104 | }, |
| 105 | { |
| 106 | name: "No match star wildcard non existent file", |
| 107 | input: "/a/1/foo*_1.txt", |
| 108 | want: []string{}, |
| 109 | }, |
| 110 | { |
| 111 | name: "No match star wildcard non existent directory", |
| 112 | input: "/a/5*/file_1.txt", |
| 113 | want: []string{}, |
| 114 | }, |
| 115 | { |
| 116 | name: "No match question wildcard non existent file", |
| 117 | input: "/a/1/file?_1.txt", |
| 118 | want: []string{}, |
| 119 | }, |
| 120 | { |
| 121 | name: "No match question wildcard non existent directory", |
| 122 | input: "/a/5?/file_1.txt", |
| 123 | want: []string{}, |
| 124 | }, |
| Brian Sheedy | b13d15c | 2025-11-20 06:56:17 -0800 | [diff] [blame] | 125 | { |
| 126 | name: "No match root dir", |
| 127 | input: "/", |
| 128 | want: []string{}, |
| 129 | }, |
| Brian Sheedy | 34355d0 | 2025-03-06 11:34:36 -0800 | [diff] [blame] | 130 | } |
| 131 | |
| 132 | for _, testCase := range tests { |
| 133 | t.Run(testCase.name, func(t *testing.T) { |
| Brian Sheedy | b13d15c | 2025-11-20 06:56:17 -0800 | [diff] [blame] | 134 | wrapper := oswrapper.CreateFSTestOSWrapper() |
| Brian Sheedy | 34355d0 | 2025-03-06 11:34:36 -0800 | [diff] [blame] | 135 | parentDirs := []string{"a", "b", "c"} |
| 136 | childDirs := []string{"1", "2", "3"} |
| 137 | fileNames := []string{"file_1.txt", "file_2.txt"} |
| 138 | for _, pd := range parentDirs { |
| 139 | for _, cd := range childDirs { |
| 140 | wrapper.MkdirAll(fmt.Sprintf("/%s/%s", pd, cd), 0o700) |
| 141 | for _, fn := range fileNames { |
| 142 | wrapper.Create(fmt.Sprintf("/%s/%s/%s", pd, cd, fn)) |
| 143 | } |
| 144 | } |
| 145 | } |
| 146 | |
| 147 | matches, err := Glob(testCase.input, wrapper) |
| 148 | require.Equal(t, testCase.want, matches) |
| 149 | if testCase.wantErr { |
| 150 | require.ErrorContains(t, err, testCase.wantErrMsg) |
| 151 | } else { |
| 152 | require.NoErrorf(t, err, "Failed to glob: %v", err) |
| 153 | } |
| 154 | }) |
| 155 | } |
| 156 | } |
| 157 | |
| 158 | func TestScan(t *testing.T) { |
| 159 | tests := []struct { |
| 160 | name string |
| 161 | root string |
| 162 | condition rule |
| 163 | want []string |
| 164 | wantErr bool |
| 165 | wantErrMsg string |
| 166 | }{ |
| 167 | { |
| 168 | name: "Scan everything", |
| 169 | root: "/", |
| 170 | condition: func(path string, cond bool) bool { return true }, |
| 171 | want: []string{ |
| 172 | ".other/1/file_1.txt", |
| 173 | ".other/1/file_2.txt", |
| 174 | ".other/2/file_1.txt", |
| 175 | ".other/2/file_2.txt", |
| 176 | "a/1/file_1.txt", |
| 177 | "a/1/file_2.txt", |
| 178 | "a/2/file_1.txt", |
| 179 | "a/2/file_2.txt", |
| 180 | "b/1/file_1.txt", |
| 181 | "b/1/file_2.txt", |
| 182 | "b/2/file_1.txt", |
| 183 | "b/2/file_2.txt", |
| 184 | }, |
| 185 | }, |
| 186 | { |
| 187 | name: "Scan nothing", |
| 188 | root: "/", |
| 189 | condition: func(path string, cond bool) bool { return false }, |
| 190 | want: []string{}, |
| 191 | }, |
| 192 | { |
| 193 | name: "Scan everything with subdir root", |
| 194 | root: "/a", |
| 195 | condition: func(path string, cond bool) bool { return true }, |
| 196 | want: []string{ |
| 197 | "1/file_1.txt", |
| 198 | "1/file_2.txt", |
| 199 | "2/file_1.txt", |
| 200 | "2/file_2.txt", |
| 201 | }, |
| 202 | }, |
| 203 | { |
| 204 | name: "Scan with rule", |
| 205 | root: "/", |
| 206 | condition: func(path string, cond bool) bool { return strings.Contains(path, "/2/") }, |
| 207 | want: []string{ |
| 208 | ".other/2/file_1.txt", |
| 209 | ".other/2/file_2.txt", |
| 210 | "a/2/file_1.txt", |
| 211 | "a/2/file_2.txt", |
| 212 | "b/2/file_1.txt", |
| 213 | "b/2/file_2.txt", |
| 214 | }, |
| 215 | }, |
| 216 | { |
| Brian Sheedy | b13d15c | 2025-11-20 06:56:17 -0800 | [diff] [blame] | 217 | // Treated as CWD. |
| 218 | // TODO(crbug.com/436025865): Update this to actually switch directories |
| 219 | // once FSTestOSWrapper is CWD-aware. |
| 220 | name: "Empty root", |
| 221 | root: "", |
| 222 | condition: func(path string, cond bool) bool { return true }, |
| 223 | want: []string{ |
| 224 | ".other/1/file_1.txt", |
| 225 | ".other/1/file_2.txt", |
| 226 | ".other/2/file_1.txt", |
| 227 | ".other/2/file_2.txt", |
| 228 | "a/1/file_1.txt", |
| 229 | "a/1/file_2.txt", |
| 230 | "a/2/file_1.txt", |
| 231 | "a/2/file_2.txt", |
| 232 | "b/1/file_1.txt", |
| 233 | "b/1/file_2.txt", |
| 234 | "b/2/file_1.txt", |
| 235 | "b/2/file_2.txt", |
| 236 | }, |
| Brian Sheedy | 34355d0 | 2025-03-06 11:34:36 -0800 | [diff] [blame] | 237 | }, |
| 238 | { |
| Brian Sheedy | b13d15c | 2025-11-20 06:56:17 -0800 | [diff] [blame] | 239 | // TODO(crbug.com/436025865): Update error message when FSTestOSWrapper |
| 240 | // properly handles leading / in errors. |
| Brian Sheedy | 34355d0 | 2025-03-06 11:34:36 -0800 | [diff] [blame] | 241 | name: "Non-existent root", |
| 242 | root: "/asdf", |
| 243 | condition: func(path string, cond bool) bool { return true }, |
| 244 | want: nil, |
| 245 | wantErr: true, |
| Brian Sheedy | b13d15c | 2025-11-20 06:56:17 -0800 | [diff] [blame] | 246 | wantErrMsg: "open asdf: file does not exist", |
| Brian Sheedy | 34355d0 | 2025-03-06 11:34:36 -0800 | [diff] [blame] | 247 | }, |
| 248 | } |
| 249 | |
| 250 | for _, testCase := range tests { |
| 251 | t.Run(testCase.name, func(t *testing.T) { |
| Brian Sheedy | b13d15c | 2025-11-20 06:56:17 -0800 | [diff] [blame] | 252 | wrapper := oswrapper.CreateFSTestOSWrapper() |
| Brian Sheedy | 34355d0 | 2025-03-06 11:34:36 -0800 | [diff] [blame] | 253 | parentDirs := []string{"a", "b", ".git", ".other"} |
| 254 | childDirs := []string{"1", "2"} |
| 255 | fileNames := []string{"file_1.txt", "file_2.txt"} |
| 256 | for _, pd := range parentDirs { |
| 257 | for _, cd := range childDirs { |
| 258 | wrapper.MkdirAll(fmt.Sprintf("/%s/%s", pd, cd), 0o700) |
| 259 | for _, fn := range fileNames { |
| 260 | wrapper.Create(fmt.Sprintf("/%s/%s/%s", pd, cd, fn)) |
| 261 | } |
| 262 | } |
| 263 | } |
| 264 | |
| 265 | matches, err := Scan(testCase.root, Config{Paths: searchRules{testCase.condition}}, wrapper) |
| 266 | require.Equal(t, testCase.want, matches) |
| 267 | if testCase.wantErr { |
| 268 | require.ErrorContains(t, err, testCase.wantErrMsg) |
| 269 | } else { |
| 270 | require.NoErrorf(t, err, "Failed to scan: %v", err) |
| 271 | } |
| 272 | }) |
| 273 | } |
| 274 | } |
| 275 | |
| 276 | func TestLoadConfig(t *testing.T) { |
| 277 | tests := []struct { |
| 278 | name string |
| 279 | cfgPath string |
| 280 | cfgJson string |
| 281 | wantNumRules int |
| 282 | wantErr bool |
| 283 | wantErrMsg string |
| 284 | }{ |
| 285 | { |
| 286 | name: "Success", |
| 287 | cfgPath: "/config.cfg", |
| 288 | // This is what was in tools/src/cmd/trim-includes/config.cfg at the time of writing. |
| 289 | cfgJson: `{ |
| 290 | "paths": [ |
| 291 | { "include": [ "src/**.h", "src/**.cc" ] }, |
| 292 | { "exclude": [ "src/**_windows.*", "src/**_other.*" ] } |
| 293 | ] |
| 294 | }`, |
| 295 | wantNumRules: 2, |
| 296 | }, |
| 297 | { |
| 298 | name: "Both include and exclude", |
| 299 | cfgPath: "/config.cfg", |
| 300 | cfgJson: `{ |
| 301 | "paths": [ |
| 302 | { "include": [ "src/**.h", "src/**.cc" ], "exclude": [ "src/**.h" ] } |
| 303 | ] |
| 304 | }`, |
| 305 | wantErr: true, |
| 306 | wantErrMsg: "Rule cannot contain both include and exclude", |
| 307 | }, |
| 308 | { |
| 309 | name: "Invalid path", |
| 310 | cfgPath: "/asdf.cfg", |
| 311 | cfgJson: "{}", |
| 312 | wantErr: true, |
| 313 | wantErrMsg: "open /asdf.cfg: file does not exist", |
| 314 | }, |
| 315 | } |
| 316 | |
| 317 | for _, testCase := range tests { |
| 318 | t.Run(testCase.name, func(t *testing.T) { |
| Brian Sheedy | b13d15c | 2025-11-20 06:56:17 -0800 | [diff] [blame] | 319 | wrapper := oswrapper.CreateFSTestOSWrapper() |
| Brian Sheedy | 34355d0 | 2025-03-06 11:34:36 -0800 | [diff] [blame] | 320 | wrapper.WriteFile("/config.cfg", []byte(testCase.cfgJson), 0o700) |
| 321 | |
| 322 | cfg, err := LoadConfig(testCase.cfgPath, wrapper) |
| 323 | // We can't check equality since the loaded Config contains function |
| 324 | // pointers to anonymous functions. So, just check that the number of |
| 325 | // rules matches. |
| Ryan Harrison | 5f85f4f | 2025-08-18 14:04:16 -0700 | [diff] [blame] | 326 | require.Len(t, cfg.Paths, testCase.wantNumRules) |
| Brian Sheedy | 34355d0 | 2025-03-06 11:34:36 -0800 | [diff] [blame] | 327 | if testCase.wantErr { |
| 328 | require.ErrorContains(t, err, testCase.wantErrMsg) |
| 329 | } else { |
| 330 | require.NoErrorf(t, err, "Failed to load config: %v", err) |
| 331 | } |
| 332 | }) |
| 333 | } |
| 334 | } |