go.fuchsia.dev/infra@v0.0.0-20240507153436-9b593402251b/cmd/roller-configurator/validate_test.go (about) 1 // Copyright 2023 The Fuchsia Authors. All rights reserved. 2 // Use of this source code is governed by a BSD-style license that can be 3 // found in the LICENSE file. 4 5 package main 6 7 import ( 8 "context" 9 "os" 10 "path/filepath" 11 "testing" 12 13 "go.fuchsia.dev/infra/cmd/roller-configurator/proto" 14 ) 15 16 var filesWithGitmodules = map[string]string{ 17 ".gitmodules": ` 18 [submodule "path/to/submodule"] 19 url = "https://example.com/asubmodule" 20 `, 21 } 22 23 var filesWithJiriManifest = map[string]string{ 24 "path/to/jiri_manifest": ` 25 <?xml version="1.0" encoding="UTF-8"?> 26 <manifest> 27 <projects> 28 <project name="foo-project" 29 remote="https://example.com/jiri-project"/> 30 </projects> 31 <packages> 32 <package name="package1"/> 33 <package name="package2"/> 34 </packages> 35 </manifest> 36 `, 37 } 38 39 func TestValidate_valid(t *testing.T) { 40 t.Parallel() 41 42 testCases := []struct { 43 name string 44 config *proto.Config 45 files map[string]string 46 }{ 47 { 48 name: "empty", 49 config: &proto.Config{}, 50 }, 51 { 52 name: "submodule with interval schedule", 53 config: &proto.Config{ 54 Rollers: []*proto.Roller{ 55 { 56 ToRoll: &proto.Roller_Submodule{Submodule: &proto.GitSubmodule{ 57 Path: "path/to/submodule", 58 }}, 59 Schedule: "with 24h interval", 60 }, 61 }, 62 }, 63 files: filesWithGitmodules, 64 }, 65 { 66 name: "cipd ensure file with cron schedule", 67 config: &proto.Config{ 68 Rollers: []*proto.Roller{ 69 { 70 ToRoll: &proto.Roller_CipdEnsureFile{CipdEnsureFile: &proto.CIPDEnsureFile{ 71 Path: "path/to/cipd.ensure", 72 Ref: "foo", 73 }}, 74 Schedule: "0/20 * * * *", 75 }, 76 }, 77 }, 78 files: map[string]string{ 79 "path/to/cipd.ensure": ``, 80 }, 81 }, 82 { 83 name: "jiri project with notify emails", 84 config: &proto.Config{ 85 Rollers: []*proto.Roller{ 86 { 87 ToRoll: &proto.Roller_JiriProject{JiriProject: &proto.JiriProject{ 88 Manifest: "path/to/jiri_manifest", 89 Project: "foo-project", 90 }}, 91 NotifyEmails: []string{ 92 "foo@example.com", 93 "bar@example.com", 94 }, 95 }, 96 }, 97 DefaultCheckoutJiriManifest: "manifest", 98 }, 99 files: filesWithJiriManifest, 100 }, 101 { 102 name: "jiri packages", 103 config: &proto.Config{ 104 Rollers: []*proto.Roller{ 105 { 106 ToRoll: &proto.Roller_JiriPackages{JiriPackages: &proto.JiriPackages{ 107 Manifests: []*proto.JiriPackages_Manifest{ 108 { 109 Path: "path/to/jiri_manifest", 110 Packages: []string{ 111 "package1", 112 "package2", 113 }, 114 }, 115 }, 116 }}, 117 }, 118 }, 119 DefaultCheckoutJiriManifest: "manifest", 120 }, 121 files: filesWithJiriManifest, 122 }, 123 } 124 125 for _, tc := range testCases { 126 t.Run(tc.name, func(t *testing.T) { 127 t.Parallel() 128 129 repoRoot := t.TempDir() 130 writeFiles(t, repoRoot, tc.files) 131 132 err := validate(context.Background(), repoRoot, tc.config) 133 if err != nil { 134 t.Fatal(err) 135 } 136 }) 137 } 138 } 139 140 func TestValidate_invalid(t *testing.T) { 141 t.Parallel() 142 143 testCases := []struct { 144 name string 145 config *proto.Config 146 err string 147 files map[string]string 148 }{ 149 { 150 name: "unnecessary default_checkout_jiri_manifest", 151 config: &proto.Config{ 152 // Shouldn't be set unless `Rollers` contains any Jiri rollers. 153 DefaultCheckoutJiriManifest: "manifest", 154 }, 155 err: "default_checkout_jiri_manifest need not be set", 156 }, 157 { 158 name: "no entity to roll", 159 config: &proto.Config{ 160 Rollers: []*proto.Roller{ 161 {}, 162 }, 163 }, 164 err: "entry 0 is missing an entity to roll", 165 }, 166 { 167 name: "invalid interval schedule", 168 config: &proto.Config{ 169 Rollers: []*proto.Roller{ 170 { 171 ToRoll: &proto.Roller_Submodule{Submodule: &proto.GitSubmodule{ 172 Path: "path/to/submodule", 173 }}, 174 Schedule: "with blah", 175 }, 176 }, 177 }, 178 files: filesWithGitmodules, 179 err: `invalid schedule "with blah"`, 180 }, 181 { 182 name: "invalid interval schedule duration", 183 config: &proto.Config{ 184 Rollers: []*proto.Roller{ 185 { 186 ToRoll: &proto.Roller_Submodule{Submodule: &proto.GitSubmodule{ 187 Path: "path/to/submodule", 188 }}, 189 Schedule: "with 6f interval", 190 }, 191 }, 192 }, 193 files: filesWithGitmodules, 194 err: `invalid duration in schedule "with 6f interval"`, 195 }, 196 { 197 name: "invalid cron schedule", 198 config: &proto.Config{ 199 Rollers: []*proto.Roller{ 200 { 201 ToRoll: &proto.Roller_Submodule{Submodule: &proto.GitSubmodule{ 202 Path: "path/to/submodule", 203 }}, 204 Schedule: "foo * * * *", 205 }, 206 }, 207 }, 208 files: filesWithGitmodules, 209 err: `invalid cron schedule "foo * * * *"`, 210 }, 211 { 212 name: "invalid notify email", 213 config: &proto.Config{ 214 Rollers: []*proto.Roller{ 215 { 216 ToRoll: &proto.Roller_Submodule{Submodule: &proto.GitSubmodule{ 217 Path: "path/to/submodule", 218 }}, 219 NotifyEmails: []string{ 220 "valid@example.com", 221 "not-an-email", 222 }, 223 }, 224 }, 225 }, 226 files: filesWithGitmodules, 227 err: `invalid email "not-an-email"`, 228 }, 229 { 230 name: "submodule with no .gitmodules file", 231 config: &proto.Config{ 232 Rollers: []*proto.Roller{ 233 { 234 ToRoll: &proto.Roller_Submodule{Submodule: &proto.GitSubmodule{ 235 Path: "path/to/submodule", 236 }}, 237 }, 238 }, 239 }, 240 err: "no .gitmodules file in repository root", 241 }, 242 { 243 name: "submodule with invalid .gitmodules file", 244 config: &proto.Config{ 245 Rollers: []*proto.Roller{ 246 { 247 ToRoll: &proto.Roller_Submodule{Submodule: &proto.GitSubmodule{ 248 Path: "path/to/submodule", 249 }}, 250 }, 251 }, 252 }, 253 files: map[string]string{ 254 ".gitmodules": `invalid`, 255 }, 256 err: "invalid `git config --list --file .gitmodules` line: \"invalid\"", 257 }, 258 { 259 name: "submodule not listed in .gitmodules", 260 config: &proto.Config{ 261 Rollers: []*proto.Roller{ 262 { 263 ToRoll: &proto.Roller_Submodule{Submodule: &proto.GitSubmodule{ 264 Path: "path/to/INVALID", 265 }}, 266 }, 267 }, 268 }, 269 files: map[string]string{ 270 ".gitmodules": ` 271 [submodule "path/to/submodule"] 272 url = "https://example.com/asubmodule" 273 `, 274 }, 275 err: `no such submodule "path/to/INVALID" listed in .gitmodules`, 276 }, 277 { 278 name: "missing CIPD ensure file", 279 config: &proto.Config{ 280 Rollers: []*proto.Roller{ 281 { 282 ToRoll: &proto.Roller_CipdEnsureFile{CipdEnsureFile: &proto.CIPDEnsureFile{ 283 Path: "path/to/cipd.ensure", 284 Ref: "foo", 285 }}, 286 }, 287 }, 288 }, 289 err: "no such file: path/to/cipd.ensure", 290 }, 291 { 292 name: "missing jiri project manifest", 293 config: &proto.Config{ 294 Rollers: []*proto.Roller{ 295 { 296 ToRoll: &proto.Roller_JiriProject{JiriProject: &proto.JiriProject{ 297 Manifest: "path/to/manifest", 298 Project: "project-name", 299 }}, 300 }, 301 }, 302 }, 303 err: "no such file: path/to/manifest", 304 }, 305 { 306 name: "missing jiri package manifest", 307 config: &proto.Config{ 308 Rollers: []*proto.Roller{ 309 { 310 ToRoll: &proto.Roller_JiriPackages{JiriPackages: &proto.JiriPackages{ 311 Manifests: []*proto.JiriPackages_Manifest{ 312 { 313 Path: "path/to/manifest", 314 Packages: []string{ 315 "package1", 316 "package2", 317 }, 318 }, 319 }, 320 }}, 321 }, 322 }, 323 }, 324 err: "no such file: path/to/manifest", 325 }, 326 { 327 name: "invalid jiri project", 328 config: &proto.Config{ 329 Rollers: []*proto.Roller{ 330 { 331 ToRoll: &proto.Roller_JiriProject{JiriProject: &proto.JiriProject{ 332 Manifest: "path/to/jiri_manifest", 333 Project: "not-a-project", 334 }}, 335 }, 336 }, 337 }, 338 files: filesWithJiriManifest, 339 err: `no project "not-a-project" in manifest "path/to/jiri_manifest"`, 340 }, 341 { 342 name: "invalid jiri package", 343 config: &proto.Config{ 344 Rollers: []*proto.Roller{ 345 { 346 ToRoll: &proto.Roller_JiriPackages{JiriPackages: &proto.JiriPackages{ 347 Manifests: []*proto.JiriPackages_Manifest{ 348 { 349 Path: "path/to/jiri_manifest", 350 Packages: []string{ 351 "bad-package1", 352 "bad-package2", 353 }, 354 }, 355 }, 356 }}, 357 }, 358 }, 359 }, 360 files: filesWithJiriManifest, 361 err: `no package "bad-package1" in manifest "path/to/jiri_manifest"`, 362 }, 363 { 364 name: "jiri project roller with no default checkout manifest", 365 config: &proto.Config{ 366 Rollers: []*proto.Roller{ 367 { 368 ToRoll: &proto.Roller_JiriProject{JiriProject: &proto.JiriProject{ 369 Manifest: "path/to/jiri_manifest", 370 Project: "foo-project", 371 }}, 372 }, 373 }, 374 }, 375 files: filesWithJiriManifest, 376 err: `default_checkout_jiri_manifest is required to enable jiri rollers`, 377 }, 378 { 379 name: "jiri package roller with no default checkout manifest", 380 config: &proto.Config{ 381 Rollers: []*proto.Roller{ 382 { 383 ToRoll: &proto.Roller_JiriPackages{JiriPackages: &proto.JiriPackages{ 384 Manifests: []*proto.JiriPackages_Manifest{ 385 { 386 Path: "path/to/jiri_manifest", 387 Packages: []string{ 388 "package1", 389 "package2", 390 }, 391 }, 392 }, 393 }}, 394 }, 395 }, 396 }, 397 files: filesWithJiriManifest, 398 err: `default_checkout_jiri_manifest is required to enable jiri rollers`, 399 }, 400 } 401 402 for _, tc := range testCases { 403 t.Run(tc.name, func(t *testing.T) { 404 t.Parallel() 405 406 repoRoot := t.TempDir() 407 writeFiles(t, repoRoot, tc.files) 408 409 err := validate(context.Background(), repoRoot, tc.config) 410 if err == nil { 411 t.Fatalf("Expected an error: %s", tc.err) 412 } 413 if err.Error() != tc.err { 414 t.Fatalf("Got error %q, expected %q", err, tc.err) 415 } 416 }) 417 } 418 } 419 420 func writeFiles(t *testing.T, rootDir string, files map[string]string) { 421 for path, contents := range files { 422 abspath := filepath.Join(rootDir, path) 423 if err := os.MkdirAll(filepath.Dir(abspath), 0o700); err != nil { 424 t.Fatal(err) 425 } 426 if err := os.WriteFile(abspath, []byte(contents), 0o600); err != nil { 427 t.Fatal(err) 428 } 429 } 430 }