github.com/canonical/ubuntu-image@v0.0.0-20240430122802-2202fe98b290/internal/statemachine/state_machine_test.go (about) 1 package statemachine 2 3 import ( 4 "fmt" 5 "io" 6 "os" 7 "os/exec" 8 "path/filepath" 9 "strings" 10 "testing" 11 12 diskfs "github.com/diskfs/go-diskfs" 13 "github.com/diskfs/go-diskfs/disk" 14 "github.com/google/go-cmp/cmp" 15 "github.com/google/go-cmp/cmp/cmpopts" 16 "github.com/google/uuid" 17 "github.com/invopop/jsonschema" 18 "github.com/snapcore/snapd/gadget" 19 "github.com/snapcore/snapd/gadget/quantity" 20 "github.com/snapcore/snapd/image" 21 "github.com/snapcore/snapd/osutil" 22 "github.com/snapcore/snapd/seed" 23 "github.com/xeipuuv/gojsonschema" 24 25 "github.com/canonical/ubuntu-image/internal/commands" 26 "github.com/canonical/ubuntu-image/internal/helper" 27 ) 28 29 const ( 30 testDataDir = "testdata" 31 ) 32 33 var testDir = "ubuntu-image-0615c8dd-d3af-4074-bfcb-c3d3c8392b06" 34 35 // for tests where we don't want to run actual states 36 var testStates = []stateFunc{ 37 {"test_succeed", func(*StateMachine) error { return nil }}, 38 } 39 40 // for tests where we want to run all the states 41 var allTestStates = []stateFunc{ 42 {prepareGadgetTreeState.name, func(statemachine *StateMachine) error { return nil }}, 43 {prepareClassicImageState.name, func(statemachine *StateMachine) error { return nil }}, 44 {loadGadgetYamlState.name, func(statemachine *StateMachine) error { return nil }}, 45 {populateClassicRootfsContentsState.name, func(statemachine *StateMachine) error { return nil }}, 46 {generateDiskInfoState.name, func(statemachine *StateMachine) error { return nil }}, 47 {calculateRootfsSizeState.name, func(statemachine *StateMachine) error { return nil }}, 48 {populateBootfsContentsState.name, func(statemachine *StateMachine) error { return nil }}, 49 {populatePreparePartitionsState.name, func(statemachine *StateMachine) error { return nil }}, 50 {makeDiskState.name, func(statemachine *StateMachine) error { return nil }}, 51 {generatePackageManifestState.name, func(statemachine *StateMachine) error { return nil }}, 52 } 53 54 func ptrToOffset(offset quantity.Offset) *quantity.Offset { 55 return &offset 56 } 57 58 // define some mocked versions of go package functions 59 func mockCopyBlob([]string) error { 60 return fmt.Errorf("Test Error") 61 } 62 func mockSetDefaults(interface{}) error { 63 return fmt.Errorf("Test Error") 64 } 65 func mockCheckEmptyFields(interface{}, *gojsonschema.Result, *jsonschema.Schema) error { 66 return fmt.Errorf("Test Error") 67 } 68 func mockCheckTags(interface{}, string) (string, error) { 69 return "", fmt.Errorf("Test Error") 70 } 71 func mockBackupAndCopyResolvConfFail(string) error { 72 return fmt.Errorf("Test Error") 73 } 74 func mockBackupAndCopyResolvConfSuccess(string) error { 75 return nil 76 } 77 func mockRestoreResolvConf(string) error { 78 return fmt.Errorf("Test Error") 79 } 80 func mockCopyBlobSuccess([]string) error { 81 return nil 82 } 83 func mockLayoutVolume(*gadget.Volume, 84 map[int]*gadget.OnDiskStructure, 85 *gadget.LayoutOptions) (*gadget.LaidOutVolume, error) { 86 return nil, fmt.Errorf("Test Error") 87 } 88 func mockNewMountedFilesystemWriter(*gadget.LaidOutStructure, *gadget.LaidOutStructure, 89 gadget.ContentObserver) (*gadget.MountedFilesystemWriter, error) { 90 return nil, fmt.Errorf("Test Error") 91 } 92 func mockMkfsWithContent(typ, img, label, contentRootDir string, deviceSize, sectorSize quantity.Size) error { 93 return fmt.Errorf("Test Error") 94 } 95 func mockMkfs(typ, img, label string, deviceSize, sectorSize quantity.Size) error { 96 return fmt.Errorf("Test Error") 97 } 98 func mockReadDir(string) ([]os.DirEntry, error) { 99 return []os.DirEntry{}, fmt.Errorf("Test Error") 100 } 101 func mockReadFile(string) ([]byte, error) { 102 return []byte{}, fmt.Errorf("Test Error") 103 } 104 func mockWriteFile(string, []byte, os.FileMode) error { 105 return fmt.Errorf("Test Error") 106 } 107 func mockMkdir(string, os.FileMode) error { 108 return fmt.Errorf("Test error") 109 } 110 func mockMkdirAll(string, os.FileMode) error { 111 return fmt.Errorf("Test error") 112 } 113 func mockMkdirTemp(string, string) (string, error) { 114 return "", fmt.Errorf("Test error") 115 } 116 func mockOpen(string) (*os.File, error) { 117 return nil, fmt.Errorf("Test error") 118 } 119 func mockOpenFile(string, int, os.FileMode) (*os.File, error) { 120 return nil, fmt.Errorf("Test error") 121 } 122 func mockOpenFileAppend(name string, flag int, perm os.FileMode) (*os.File, error) { 123 return os.OpenFile(name, flag|os.O_APPEND, perm) 124 } 125 func mockRemoveAll(string) error { 126 return fmt.Errorf("Test error") 127 } 128 func mockRename(string, string) error { 129 return fmt.Errorf("Test error") 130 } 131 func mockTruncate(string, int64) error { 132 return fmt.Errorf("Test error") 133 } 134 func mockCreate(string) (*os.File, error) { 135 return nil, fmt.Errorf("Test error") 136 } 137 func mockCopyFile(string, string, osutil.CopyFlag) error { 138 return fmt.Errorf("Test error") 139 } 140 func mockCopySpecialFile(string, string) error { 141 return fmt.Errorf("Test error") 142 } 143 func mockDiskfsCreate(string, int64, diskfs.Format, diskfs.SectorSize) (*disk.Disk, error) { 144 return nil, fmt.Errorf("Test error") 145 } 146 func mockRandRead(output []byte) (int, error) { 147 return 0, fmt.Errorf("Test error") 148 } 149 func mockSeedOpen(seedDir, label string) (seed.Seed, error) { 150 return nil, fmt.Errorf("Test error") 151 } 152 func mockImagePrepare(*image.Options) error { 153 return fmt.Errorf("Test Error") 154 } 155 func mockMarshal(interface{}) ([]byte, error) { 156 return []byte{}, fmt.Errorf("Test Error") 157 } 158 func mockRel(string, string) (string, error) { 159 return "", fmt.Errorf("Test error") 160 } 161 func mockGojsonschemaValidateError(gojsonschema.JSONLoader, gojsonschema.JSONLoader) (*gojsonschema.Result, error) { 162 return nil, fmt.Errorf("Test Error") 163 } 164 165 func readOnlyDiskfsCreate(diskName string, size int64, format diskfs.Format, sectorSize diskfs.SectorSize) (*disk.Disk, error) { 166 diskFile, err := os.OpenFile(diskName, os.O_RDONLY|os.O_CREATE, 0444) 167 disk := disk.Disk{ 168 File: diskFile, 169 LogicalBlocksize: int64(sectorSize), 170 } 171 return &disk, err 172 } 173 174 // Fake exec command helper 175 var testCaseName string 176 177 func fakeExecCommand(command string, args ...string) *exec.Cmd { 178 cs := []string{"-test.run=TestExecHelperProcess", "--", command} 179 cs = append(cs, args...) 180 //nolint:gosec,G204 181 cmd := exec.Command(os.Args[0], cs...) 182 tc := "TEST_CASE=" + testCaseName 183 cmd.Env = []string{"GO_WANT_HELPER_PROCESS=1", tc} 184 return cmd 185 } 186 187 // helper function to define *quantity.Offsets inline 188 func createOffsetPointer(x quantity.Offset) *quantity.Offset { 189 return &x 190 } 191 192 // This is a helper that mocks out any exec calls performed in this package 193 func TestExecHelperProcess(t *testing.T) { 194 if os.Getenv("GO_WANT_HELPER_PROCESS") != "1" { 195 return 196 } 197 defer os.Exit(0) 198 args := os.Args 199 200 // We need to get rid of the trailing 'mock' call of our test binary, so 201 // that args has the actual command arguments. We can then check their 202 // correctness etc. 203 for len(args) > 0 { 204 if args[0] == "--" { 205 args = args[1:] 206 break 207 } 208 args = args[1:] 209 } 210 211 // I think the best idea I saw from people is to switch this on test case 212 // instead on the actual arguments. And this makes sense to me 213 switch os.Getenv("TEST_CASE") { 214 case "TestGeneratePackageManifest": 215 fmt.Fprint(os.Stdout, "foo 1.2\nbar 1.4-1ubuntu4.1\nlibbaz 0.1.3ubuntu2\n") 216 case "TestGenerateFilelist": 217 fmt.Fprint(os.Stdout, "/root\n/home\n/var") 218 case "TestFailedPreseedClassicImage", 219 "TestFailedUpdateGrubLosetup", 220 "TestFailedMakeQcow2Image", 221 "TestFailedGeneratePackageManifest", 222 "TestFailedGenerateFilelist", 223 "TestFailedGerminate", 224 "TestFailedSetupLiveBuildCommands", 225 "TestFailedCreateChroot", 226 "TestStateMachine_installPackages_fail", 227 "TestFailedPrepareClassicImage", 228 "TestFailedBuildGadgetTree": 229 // throwing an error here simulates the "command" having an error 230 os.Exit(1) 231 case "TestFailedUpdateGrubOther": // this passes the initial losetup command and fails a later command 232 if args[0] != "losetup" { 233 os.Exit(1) 234 } 235 case "TestFailedCreateChrootNoHostname", 236 "TestFailedCreateChrootSkip", 237 "TestFailedRunLiveBuild": 238 // Do nothing so we don't have to wait for actual lb commands 239 break 240 } 241 } 242 243 // testStateMachine implements Setup, Run, and Teardown() for testing purposes 244 type testStateMachine struct { 245 StateMachine 246 } 247 248 // testStateMachine needs its own setup 249 func (TestStateMachine *testStateMachine) Setup() error { 250 // set the states that will be used for this image type 251 TestStateMachine.states = allTestStates 252 253 // do the validation common to all image types 254 if err := TestStateMachine.validateInput(); err != nil { 255 return err 256 } 257 258 // if --resume was passed, figure out where to start 259 if err := TestStateMachine.readMetadata(metadataStateFile); err != nil { 260 return err 261 } 262 263 return nil 264 } 265 266 // TestUntilThru tests --until and --thru with each state 267 func TestUntilThru(t *testing.T) { 268 testCases := []struct { 269 name string 270 }{ 271 {"until"}, 272 {"thru"}, 273 } 274 for _, tc := range testCases { 275 t.Run("test "+tc.name, func(t *testing.T) { 276 for _, state := range allTestStates { 277 asserter := helper.Asserter{T: t} 278 // run a partial state machine 279 var partialStateMachine testStateMachine 280 partialStateMachine.commonFlags, partialStateMachine.stateMachineFlags = helper.InitCommonOpts() 281 tempDir := filepath.Join("/tmp", "ubuntu-image-"+tc.name) 282 if err := os.Mkdir(tempDir, 0755); err != nil { 283 t.Errorf("Could not create workdir: %s\n", err.Error()) 284 } 285 t.Cleanup(func() { os.RemoveAll(tempDir) }) 286 partialStateMachine.stateMachineFlags.WorkDir = tempDir 287 288 if tc.name == "until" { 289 partialStateMachine.stateMachineFlags.Until = state.name 290 } else { 291 partialStateMachine.stateMachineFlags.Thru = state.name 292 } 293 294 err := partialStateMachine.Setup() 295 asserter.AssertErrNil(err, false) 296 297 err = partialStateMachine.Run() 298 asserter.AssertErrNil(err, false) 299 300 err = partialStateMachine.Teardown() 301 asserter.AssertErrNil(err, false) 302 303 // now resume 304 var resumeStateMachine testStateMachine 305 resumeStateMachine.commonFlags, resumeStateMachine.stateMachineFlags = helper.InitCommonOpts() 306 resumeStateMachine.stateMachineFlags.Resume = true 307 resumeStateMachine.stateMachineFlags.WorkDir = partialStateMachine.stateMachineFlags.WorkDir 308 309 err = resumeStateMachine.Setup() 310 asserter.AssertErrNil(err, false) 311 312 err = resumeStateMachine.Run() 313 asserter.AssertErrNil(err, false) 314 315 err = resumeStateMachine.Teardown() 316 asserter.AssertErrNil(err, false) 317 318 os.RemoveAll(tempDir) 319 } 320 }) 321 } 322 } 323 324 // TestDebug ensures that the name of the states is printed when the --debug flag is used 325 func TestDebug(t *testing.T) { 326 asserter := helper.Asserter{T: t} 327 workDir := "ubuntu-image-test-debug" 328 if err := os.Mkdir(workDir, 0755); err != nil { 329 t.Errorf("Failed to create temporary directory %s\n", workDir) 330 } 331 332 t.Cleanup(func() { os.RemoveAll(workDir) }) 333 334 var stateMachine testStateMachine 335 stateMachine.commonFlags, stateMachine.stateMachineFlags = helper.InitCommonOpts() 336 stateMachine.stateMachineFlags.WorkDir = workDir 337 stateMachine.commonFlags.Debug = true 338 339 err := stateMachine.Setup() 340 asserter.AssertErrNil(err, true) 341 342 // just use the one state 343 stateMachine.states = testStates 344 stdout, restoreStdout, err := helper.CaptureStd(&os.Stdout) 345 asserter.AssertErrNil(err, true) 346 347 err = stateMachine.Run() 348 asserter.AssertErrNil(err, true) 349 350 // restore stdout and check that the debug info was printed 351 restoreStdout() 352 readStdout, err := io.ReadAll(stdout) 353 asserter.AssertErrNil(err, true) 354 355 if !strings.Contains(string(readStdout), stateMachine.states[0].name) { 356 t.Errorf("Expected state name \"%s\" to appear in output \"%s\"\n", stateMachine.states[0].name, string(readStdout)) 357 } 358 } 359 360 // TestDryRun ensures that the name of the states is not printed when the --dry-run flag is used 361 // because nothing should be executed 362 func TestDryRun(t *testing.T) { 363 asserter := helper.Asserter{T: t} 364 workDir := "ubuntu-image-test-debug" 365 err := os.Mkdir(workDir, 0755) 366 asserter.AssertErrNil(err, true) 367 368 t.Cleanup(func() { os.RemoveAll(workDir) }) 369 370 var stateMachine testStateMachine 371 stateMachine.commonFlags, stateMachine.stateMachineFlags = helper.InitCommonOpts() 372 stateMachine.stateMachineFlags.WorkDir = workDir 373 stateMachine.commonFlags.DryRun = true 374 375 err = stateMachine.Setup() 376 asserter.AssertErrNil(err, true) 377 378 // just use the one state 379 stateMachine.states = testStates 380 stdout, restoreStdout, err := helper.CaptureStd(&os.Stdout) 381 asserter.AssertErrNil(err, true) 382 383 err = stateMachine.Run() 384 asserter.AssertErrNil(err, true) 385 386 restoreStdout() 387 readStdout, err := io.ReadAll(stdout) 388 asserter.AssertErrNil(err, true) 389 390 if strings.Contains(string(readStdout), stateMachine.states[0].name) { 391 t.Errorf("Expected state name \"%s\" to not appear in output \"%s\"\n", stateMachine.states[0].name, string(readStdout)) 392 } 393 } 394 395 // TestFunction replaces some of the stateFuncs to test various error scenarios 396 func TestFunctionErrors(t *testing.T) { 397 testCases := []struct { 398 name string 399 overrideState int 400 newStateFunc stateFunc 401 }{ 402 {"error_state_func", 0, stateFunc{"test_error_state_func", func(stateMachine *StateMachine) error { return fmt.Errorf("Test Error") }}}, 403 {"error_write_metadata", 8, stateFunc{"test_error_write_metadata", func(stateMachine *StateMachine) error { 404 os.RemoveAll(stateMachine.stateMachineFlags.WorkDir) 405 return nil 406 }}}, 407 } 408 for _, tc := range testCases { 409 t.Run("test "+tc.name, func(t *testing.T) { 410 asserter := helper.Asserter{T: t} 411 workDir := filepath.Join("/tmp", "ubuntu-image-"+tc.name) 412 err := os.Mkdir(workDir, 0755) 413 asserter.AssertErrNil(err, true) 414 415 t.Cleanup(func() { os.RemoveAll(workDir) }) 416 417 var stateMachine testStateMachine 418 stateMachine.commonFlags, stateMachine.stateMachineFlags = helper.InitCommonOpts() 419 stateMachine.stateMachineFlags.WorkDir = workDir 420 err = stateMachine.Setup() 421 asserter.AssertErrNil(err, true) 422 423 // override the function, but save the old one 424 oldStateFunc := stateMachine.states[tc.overrideState] 425 stateMachine.states[tc.overrideState] = tc.newStateFunc 426 defer func() { 427 stateMachine.states[tc.overrideState] = oldStateFunc 428 }() 429 if err := stateMachine.Run(); err == nil { 430 if err := stateMachine.Teardown(); err == nil { 431 t.Errorf("Expected an error but there was none") 432 } 433 } 434 }) 435 } 436 } 437 438 // TestSetCommonOpts ensures that the function actually sets the correct values in the struct 439 func TestSetCommonOpts(t *testing.T) { 440 asserter := helper.Asserter{T: t} 441 type args struct { 442 stateMachine SmInterface 443 commonOpts *commands.CommonOpts 444 stateMachineOpts *commands.StateMachineOpts 445 } 446 447 cmpOpts := []cmp.Option{ 448 cmp.AllowUnexported( 449 StateMachine{}, 450 temporaryDirectories{}, 451 ), 452 cmpopts.IgnoreUnexported( 453 gadget.Info{}, 454 ), 455 } 456 457 tests := []struct { 458 name string 459 args args 460 want SmInterface 461 expectedErr string 462 }{ 463 { 464 name: "set options on a snap state machine", 465 args: args{ 466 stateMachine: &SnapStateMachine{}, 467 commonOpts: &commands.CommonOpts{ 468 Debug: true, 469 }, 470 stateMachineOpts: &commands.StateMachineOpts{ 471 WorkDir: "workdir", 472 }, 473 }, 474 want: &SnapStateMachine{ 475 StateMachine: StateMachine{ 476 commonFlags: &commands.CommonOpts{ 477 Debug: true, 478 }, 479 stateMachineFlags: &commands.StateMachineOpts{ 480 WorkDir: "workdir", 481 }, 482 }, 483 }, 484 }, 485 { 486 name: "set options on a classic state machine", 487 args: args{ 488 stateMachine: &ClassicStateMachine{}, 489 commonOpts: &commands.CommonOpts{ 490 Debug: true, 491 }, 492 stateMachineOpts: &commands.StateMachineOpts{ 493 WorkDir: "workdir", 494 }, 495 }, 496 want: &ClassicStateMachine{ 497 StateMachine: StateMachine{ 498 commonFlags: &commands.CommonOpts{ 499 Debug: true, 500 }, 501 stateMachineFlags: &commands.StateMachineOpts{ 502 WorkDir: "workdir", 503 }, 504 }, 505 }, 506 }, 507 } 508 for _, tc := range tests { 509 t.Run(tc.name, func(t *testing.T) { 510 tc.args.stateMachine.SetCommonOpts(tc.args.commonOpts, tc.args.stateMachineOpts) 511 asserter.AssertEqual(tc.want, tc.args.stateMachine, cmpOpts...) 512 }) 513 } 514 } 515 516 // TestParseImageSizes tests a successful image size parse with all of the different allowed syntaxes 517 func TestParseImageSizes(t *testing.T) { 518 testCases := []struct { 519 name string 520 size string 521 result map[string]quantity.Size 522 }{ 523 {"one_size", "4G", map[string]quantity.Size{ 524 "first": 4 * quantity.SizeGiB, 525 "second": 4 * quantity.SizeGiB, 526 "third": 4 * quantity.SizeGiB, 527 "fourth": 4 * quantity.SizeGiB}}, 528 {"size_per_image_name", "first:1G,second:2G,third:3G,fourth:4G", map[string]quantity.Size{ 529 "first": 1 * quantity.SizeGiB, 530 "second": 2 * quantity.SizeGiB, 531 "third": 3 * quantity.SizeGiB, 532 "fourth": 4 * quantity.SizeGiB}}, 533 {"size_per_image_number", "0:1G,1:2G,2:3G,3:4G", map[string]quantity.Size{ 534 "first": 1 * quantity.SizeGiB, 535 "second": 2 * quantity.SizeGiB, 536 "third": 3 * quantity.SizeGiB, 537 "fourth": 4 * quantity.SizeGiB}}, 538 {"mixed_size_syntax", "0:1G,second:2G,2:3G,fourth:4G", map[string]quantity.Size{ 539 "first": 1 * quantity.SizeGiB, 540 "second": 2 * quantity.SizeGiB, 541 "third": 3 * quantity.SizeGiB, 542 "fourth": 4 * quantity.SizeGiB}}, 543 } 544 for _, tc := range testCases { 545 t.Run("test_parse_image_sizes_"+tc.name, func(t *testing.T) { 546 asserter := helper.Asserter{T: t} 547 var stateMachine StateMachine 548 stateMachine.commonFlags, stateMachine.stateMachineFlags = helper.InitCommonOpts() 549 stateMachine.YamlFilePath = filepath.Join("testdata", "gadget-multi.yaml") 550 stateMachine.commonFlags.Size = tc.size 551 552 // need workdir and loaded gadget.yaml set up for this 553 err := stateMachine.makeTemporaryDirectories() 554 asserter.AssertErrNil(err, false) 555 556 err = stateMachine.loadGadgetYaml() 557 asserter.AssertErrNil(err, false) 558 559 err = stateMachine.parseImageSizes() 560 asserter.AssertErrNil(err, false) 561 562 // ensure the correct size was set 563 for volumeName := range stateMachine.GadgetInfo.Volumes { 564 setSize := stateMachine.ImageSizes[volumeName] 565 if setSize != tc.result[volumeName] { 566 t.Errorf("Volume %s has the wrong size set: %d", volumeName, setSize) 567 } 568 } 569 570 }) 571 } 572 } 573 574 // TestFailedParseImageSizes tests failures in parsing the image sizes 575 func TestFailedParseImageSizes(t *testing.T) { 576 testCases := []struct { 577 name string 578 size string 579 errMsg string 580 }{ 581 {"invalid_size", "4test", "Failed to parse argument to --image-size"}, 582 {"too_many_args", "first:1G:2G", "Argument to --image-size first:1G:2G is not in the correct format"}, 583 {"multiple_invalid", "first:1test", "Failed to parse argument to --image-size"}, 584 {"volume_not_exist", "fifth:1G", "Volume fifth does not exist in gadget.yaml"}, 585 {"index_out_of_range", "9:1G", "Volume index 9 is out of range"}, 586 } 587 for _, tc := range testCases { 588 t.Run("test_failed_parse_image_sizes_"+tc.name, func(t *testing.T) { 589 asserter := helper.Asserter{T: t} 590 var stateMachine StateMachine 591 stateMachine.commonFlags, stateMachine.stateMachineFlags = helper.InitCommonOpts() 592 stateMachine.YamlFilePath = filepath.Join("testdata", "gadget-multi.yaml") 593 594 // need workdir and loaded gadget.yaml set up for this 595 err := stateMachine.makeTemporaryDirectories() 596 asserter.AssertErrNil(err, false) 597 598 err = stateMachine.loadGadgetYaml() 599 asserter.AssertErrNil(err, false) 600 601 // run parseImage size and make sure it failed 602 stateMachine.commonFlags.Size = tc.size 603 err = stateMachine.parseImageSizes() 604 asserter.AssertErrContains(err, tc.errMsg) 605 }) 606 } 607 } 608 609 // TestHandleContentSizes ensures that using --image-size with a few different values 610 // results in the correct sizes in stateMachine.ImageSizes 611 func TestHandleContentSizes(t *testing.T) { 612 testCases := []struct { 613 name string 614 size string 615 result map[string]quantity.Size 616 }{ 617 {"size_not_specified", "", map[string]quantity.Size{"pc": 17825792}}, 618 {"size_smaller_than_content", "pc:123", map[string]quantity.Size{"pc": 17825792}}, 619 {"size_bigger_than_content", "pc:4G", map[string]quantity.Size{"pc": 4 * quantity.SizeGiB}}, 620 } 621 for _, tc := range testCases { 622 t.Run("test_handle_content_sizes_"+tc.name, func(t *testing.T) { 623 asserter := helper.Asserter{T: t} 624 var stateMachine StateMachine 625 stateMachine.commonFlags, stateMachine.stateMachineFlags = helper.InitCommonOpts() 626 stateMachine.commonFlags.Size = tc.size 627 stateMachine.YamlFilePath = filepath.Join("testdata", "gadget_tree", 628 "meta", "gadget.yaml") 629 630 // need workdir and loaded gadget.yaml set up for this 631 err := stateMachine.makeTemporaryDirectories() 632 asserter.AssertErrNil(err, false) 633 634 err = stateMachine.loadGadgetYaml() 635 asserter.AssertErrNil(err, false) 636 637 stateMachine.handleContentSizes(0, "pc") 638 // ensure the correct size was set 639 for volumeName := range stateMachine.GadgetInfo.Volumes { 640 setSize := stateMachine.ImageSizes[volumeName] 641 if setSize != tc.result[volumeName] { 642 t.Errorf("Volume %s has the wrong size set: %d. "+ 643 "Should be %d", volumeName, setSize, tc.result[volumeName]) 644 } 645 } 646 }) 647 } 648 } 649 650 // TestStateMachine_postProcessGadgetYaml tests postProcessGadgetYaml 651 func TestStateMachine_postProcessGadgetYaml(t *testing.T) { 652 cmpOpts := []cmp.Option{ 653 cmpopts.IgnoreUnexported( 654 gadget.Volume{}, 655 ), 656 cmpopts.IgnoreFields(gadget.VolumeStructure{}, "EnclosingVolume"), 657 } 658 tests := []struct { 659 name string 660 gadgetYaml []byte 661 wantVolumes map[string]*gadget.Volume 662 wantIsSeeded bool 663 expectedErr string 664 }{ 665 { 666 name: "simple full test", 667 gadgetYaml: []byte(`volumes: 668 pc: 669 bootloader: grub 670 structure: 671 - name: mbr 672 type: mbr 673 size: 440 674 update: 675 edition: 1 676 content: 677 - image: pc-boot.img 678 - name: BIOS Boot 679 type: DA,21686148-6449-6E6F-744E-656564454649 680 size: 1M 681 offset: 1M 682 offset-write: mbr+92 683 update: 684 edition: 2 685 content: 686 - image: pc-core.img 687 - name: ubuntu-seed 688 role: system-seed 689 filesystem: vfat 690 # UEFI will boot the ESP partition by default first 691 type: EF,C12A7328-F81F-11D2-BA4B-00A0C93EC93B 692 size: 1200M 693 update: 694 edition: 2 695 content: 696 - source: grubx64.efi 697 target: EFI/boot/grubx64.efi 698 - source: shim.efi.signed 699 target: EFI/boot/bootx64.efi 700 - name: ubuntu-boot 701 filesystem-label: system-boot 702 filesystem: ext4 703 type: 83,0FC63DAF-8483-4772-8E79-3D69D8477DE4 704 # whats the appropriate size? 705 size: 750M 706 update: 707 edition: 1 708 content: 709 - source: grubx64.efi 710 target: EFI/boot/grubx64.efi 711 - source: shim.efi.signed 712 target: EFI/boot/bootx64.efi 713 - name: ubuntu-save 714 role: system-save 715 filesystem: ext4 716 type: 83,0FC63DAF-8483-4772-8E79-3D69D8477DE4 717 size: 16M 718 - name: ubuntu-data 719 role: system-data 720 filesystem: ext4 721 type: 83,0FC63DAF-8483-4772-8E79-3D69D8477DE4 722 size: 1G 723 `), 724 wantIsSeeded: true, 725 wantVolumes: map[string]*gadget.Volume{ 726 "pc": { 727 Schema: "gpt", 728 Bootloader: "grub", 729 Structure: []gadget.VolumeStructure{ 730 { 731 VolumeName: "pc", 732 Name: "mbr", 733 Offset: createOffsetPointer(0), 734 MinSize: 440, 735 Size: 440, 736 Type: "mbr", 737 Role: "mbr", 738 Content: []gadget.VolumeContent{ 739 { 740 Image: "pc-boot.img", 741 }, 742 }, 743 Update: gadget.VolumeUpdate{Edition: 1}, 744 }, 745 { 746 VolumeName: "pc", 747 Name: "BIOS Boot", 748 Offset: createOffsetPointer(1048576), 749 OffsetWrite: &gadget.RelativeOffset{ 750 RelativeTo: "mbr", 751 Offset: quantity.Offset(92), 752 }, 753 MinSize: 1048576, 754 Size: 1048576, 755 Type: "DA,21686148-6449-6E6F-744E-656564454649", 756 Content: []gadget.VolumeContent{ 757 { 758 Image: "pc-core.img", 759 }, 760 }, 761 Update: gadget.VolumeUpdate{Edition: 2}, 762 YamlIndex: 1, 763 }, 764 { 765 VolumeName: "pc", 766 Name: "ubuntu-seed", 767 Label: "ubuntu-seed", 768 Offset: createOffsetPointer(2097152), 769 MinSize: 1258291200, 770 Size: 1258291200, 771 Type: "EF,C12A7328-F81F-11D2-BA4B-00A0C93EC93B", 772 Role: "system-seed", 773 Filesystem: "vfat", 774 Content: []gadget.VolumeContent{ 775 { 776 UnresolvedSource: "grubx64.efi", 777 Target: "EFI/boot/grubx64.efi", 778 }, 779 { 780 UnresolvedSource: "shim.efi.signed", 781 Target: "EFI/boot/bootx64.efi", 782 }, 783 }, 784 Update: gadget.VolumeUpdate{Edition: 2}, 785 YamlIndex: 2, 786 }, 787 { 788 VolumeName: "pc", 789 Name: "ubuntu-boot", 790 Offset: createOffsetPointer(1260388352), 791 MinSize: 786432000, 792 Size: 786432000, 793 Type: "83,0FC63DAF-8483-4772-8E79-3D69D8477DE4", 794 Label: "system-boot", 795 Filesystem: "ext4", 796 Content: []gadget.VolumeContent{ 797 { 798 UnresolvedSource: "grubx64.efi", 799 Target: "EFI/boot/grubx64.efi", 800 }, 801 { 802 UnresolvedSource: "shim.efi.signed", 803 Target: "EFI/boot/bootx64.efi", 804 }, 805 }, 806 Update: gadget.VolumeUpdate{Edition: 1}, 807 YamlIndex: 3, 808 }, 809 { 810 VolumeName: "pc", 811 Name: "ubuntu-save", 812 Offset: createOffsetPointer(2046820352), 813 MinSize: 16777216, 814 Size: 16777216, 815 Type: "83,0FC63DAF-8483-4772-8E79-3D69D8477DE4", 816 Role: "system-save", 817 Filesystem: "ext4", 818 YamlIndex: 4, 819 }, 820 { 821 VolumeName: "pc", 822 Name: "ubuntu-data", 823 Offset: createOffsetPointer(2063597568), 824 MinSize: 1073741824, 825 Size: 1073741824, 826 Type: "83,0FC63DAF-8483-4772-8E79-3D69D8477DE4", 827 Role: "system-data", 828 Filesystem: "ext4", 829 Content: []gadget.VolumeContent{}, 830 YamlIndex: 5, 831 }, 832 }, 833 Name: "pc", 834 }, 835 }, 836 }, 837 { 838 name: "minimal configuration, adding a system-data structure and missing content on system-seed", 839 gadgetYaml: []byte(`volumes: 840 pc: 841 bootloader: grub 842 structure: 843 - name: mbr 844 type: mbr 845 size: 440 846 update: 847 edition: 1 848 content: 849 - image: pc-boot.img 850 - name: ubuntu-seed 851 role: system-seed 852 filesystem: vfat 853 # UEFI will boot the ESP partition by default first 854 type: EF,C12A7328-F81F-11D2-BA4B-00A0C93EC93B 855 size: 1200M 856 update: 857 edition: 2 858 `), 859 wantIsSeeded: true, 860 wantVolumes: map[string]*gadget.Volume{ 861 "pc": { 862 Schema: "gpt", 863 Bootloader: "grub", 864 Structure: []gadget.VolumeStructure{ 865 { 866 VolumeName: "pc", 867 Name: "mbr", 868 Offset: createOffsetPointer(0), 869 MinSize: 440, 870 Size: 440, 871 Type: "mbr", 872 Role: "mbr", 873 Content: []gadget.VolumeContent{ 874 { 875 Image: "pc-boot.img", 876 }, 877 }, 878 Update: gadget.VolumeUpdate{Edition: 1}, 879 }, 880 { 881 VolumeName: "pc", 882 Name: "ubuntu-seed", 883 Label: "ubuntu-seed", 884 Offset: createOffsetPointer(1048576), 885 MinSize: 1258291200, 886 Size: 1258291200, 887 Type: "EF,C12A7328-F81F-11D2-BA4B-00A0C93EC93B", 888 Role: "system-seed", 889 Filesystem: "vfat", 890 Content: []gadget.VolumeContent{}, 891 Update: gadget.VolumeUpdate{Edition: 2}, 892 YamlIndex: 1, 893 }, 894 { 895 VolumeName: "", 896 Name: "", 897 Label: "writable", 898 Offset: createOffsetPointer(1259339776), 899 MinSize: 0, 900 Size: 0, 901 Type: "83,0FC63DAF-8483-4772-8E79-3D69D8477DE4", 902 Role: "system-data", 903 Filesystem: "ext4", 904 Content: []gadget.VolumeContent{}, 905 YamlIndex: 2, 906 }, 907 }, 908 Name: "pc", 909 }, 910 }, 911 }, 912 { 913 name: "error with invalid source path", 914 gadgetYaml: []byte(`volumes: 915 pc: 916 bootloader: grub 917 structure: 918 - name: ubuntu-seed 919 role: system-seed 920 filesystem: vfat 921 # UEFI will boot the ESP partition by default first 922 type: EF,C12A7328-F81F-11D2-BA4B-00A0C93EC93B 923 size: 1200M 924 update: 925 edition: 2 926 content: 927 - source: ../grubx64.efi 928 target: EFI/boot/grubx64.efi 929 `), 930 wantIsSeeded: true, 931 expectedErr: "filesystem content source \"../grubx64.efi\" contains \"../\". This is disallowed for security purposes", 932 }, 933 } 934 for _, tt := range tests { 935 t.Run(tt.name, func(t *testing.T) { 936 asserter := helper.Asserter{T: t} 937 stateMachine := &StateMachine{ 938 VolumeOrder: []string{"pc"}, 939 } 940 stateMachine.commonFlags, stateMachine.stateMachineFlags = helper.InitCommonOpts() 941 942 err := stateMachine.makeTemporaryDirectories() 943 asserter.AssertErrNil(err, false) 944 t.Cleanup(func() { os.RemoveAll(stateMachine.stateMachineFlags.WorkDir) }) 945 946 stateMachine.GadgetInfo, err = gadget.InfoFromGadgetYaml(tt.gadgetYaml, nil) 947 asserter.AssertErrNil(err, false) 948 949 err = stateMachine.postProcessGadgetYaml() 950 951 if len(tt.expectedErr) == 0 { 952 asserter.AssertErrNil(err, true) 953 asserter.AssertEqual(tt.wantIsSeeded, stateMachine.IsSeeded) 954 asserter.AssertEqual(tt.wantVolumes, stateMachine.GadgetInfo.Volumes, cmpOpts...) 955 } else { 956 asserter.AssertErrContains(err, tt.expectedErr) 957 } 958 }) 959 } 960 } 961 962 // TestStateMachine_postProcessGadgetYaml_fail tests failues in the post processing of 963 // the gadget.yaml file after loading it in. 964 func TestStateMachine_postProcessGadgetYaml_fail(t *testing.T) { 965 asserter := helper.Asserter{T: t} 966 var stateMachine StateMachine 967 stateMachine.commonFlags, stateMachine.stateMachineFlags = helper.InitCommonOpts() 968 969 err := stateMachine.makeTemporaryDirectories() 970 asserter.AssertErrNil(err, false) 971 972 // set a valid yaml file and load it in 973 stateMachine.YamlFilePath = filepath.Join("testdata", 974 "gadget_tree", "meta", "gadget.yaml") 975 // ensure unpack exists 976 err = os.MkdirAll(stateMachine.tempDirs.unpack, 0755) 977 asserter.AssertErrNil(err, true) 978 err = stateMachine.loadGadgetYaml() 979 asserter.AssertErrNil(err, false) 980 981 // mock filepath.Rel 982 filepathRel = mockRel 983 defer func() { 984 filepathRel = filepath.Rel 985 }() 986 err = stateMachine.postProcessGadgetYaml() 987 asserter.AssertErrContains(err, "Error creating relative path") 988 filepathRel = filepath.Rel 989 990 // mock os.MkdirAll 991 osMkdirAll = mockMkdirAll 992 defer func() { 993 osMkdirAll = os.MkdirAll 994 }() 995 err = stateMachine.postProcessGadgetYaml() 996 asserter.AssertErrContains(err, "Error creating volume dir") 997 osMkdirAll = os.MkdirAll 998 999 // use a gadget with a disallowed string in the content field 1000 stateMachine.YamlFilePath = filepath.Join("testdata", "gadget_invalid_content.yaml") 1001 err = stateMachine.loadGadgetYaml() 1002 asserter.AssertErrContains(err, "disallowed for security purposes") 1003 } 1004 1005 func TestStateMachine_readMetadata(t *testing.T) { 1006 type args struct { 1007 metadataFile string 1008 resume bool 1009 } 1010 1011 cmpOpts := []cmp.Option{ 1012 cmp.AllowUnexported( 1013 StateMachine{}, 1014 gadget.Info{}, 1015 temporaryDirectories{}, 1016 stateFunc{}, 1017 ), 1018 cmpopts.IgnoreFields(stateFunc{}, "function"), 1019 cmpopts.IgnoreFields(gadget.VolumeStructure{}, "EnclosingVolume"), 1020 } 1021 1022 testCases := []struct { 1023 name string 1024 wantStateMachine *StateMachine 1025 args args 1026 shouldPass bool 1027 expectedError string 1028 }{ 1029 { 1030 name: "successful read", 1031 args: args{ 1032 metadataFile: "successful_read.json", 1033 resume: true, 1034 }, 1035 wantStateMachine: &StateMachine{ 1036 stateMachineFlags: &commands.StateMachineOpts{ 1037 Resume: true, 1038 WorkDir: filepath.Join(testDataDir, "metadata"), 1039 }, 1040 CurrentStep: "", 1041 StepsTaken: 2, 1042 YamlFilePath: "/tmp/ubuntu-image-2329554237/unpack/gadget/meta/gadget.yaml", 1043 IsSeeded: true, 1044 SectorSize: quantity.Size(512), 1045 RootfsSize: quantity.Size(775915520), 1046 states: allTestStates[2:], 1047 GadgetInfo: &gadget.Info{ 1048 Volumes: map[string]*gadget.Volume{ 1049 "pc": { 1050 Schema: "gpt", 1051 Bootloader: "grub", 1052 Structure: []gadget.VolumeStructure{ 1053 { 1054 Name: "mbr", 1055 Offset: ptrToOffset(quantity.Offset(quantity.Size(0))), 1056 MinSize: quantity.Size(440), 1057 Size: quantity.Size(440), 1058 Role: "mbr", 1059 Type: "mbr", 1060 Content: []gadget.VolumeContent{ 1061 { 1062 Image: "pc-boot.img", 1063 }, 1064 }, 1065 Update: gadget.VolumeUpdate{ 1066 Edition: 1, 1067 }, 1068 YamlIndex: 0, 1069 }, 1070 { 1071 Name: "BIOS Boot", 1072 Offset: ptrToOffset(quantity.Offset(quantity.Size(1048576))), 1073 MinSize: quantity.Size(1048576), 1074 Size: quantity.Size(1048576), 1075 Role: "", 1076 Type: "21686148-6449-6E6F-744E-656564454649", 1077 Content: nil, 1078 Update: gadget.VolumeUpdate{ 1079 Edition: 2, 1080 }, 1081 YamlIndex: 1, 1082 }, 1083 }, 1084 }, 1085 }, 1086 }, 1087 ImageSizes: map[string]quantity.Size{"pc": 3155165184}, 1088 VolumeOrder: []string{"pc"}, 1089 VolumeNames: map[string]string{"pc": "pc.img"}, 1090 Packages: []string{"nginx", "apache2"}, 1091 Snaps: []string{"core", "lxd"}, 1092 tempDirs: temporaryDirectories{ 1093 rootfs: filepath.Join(testDataDir, "metadata", "root"), 1094 unpack: filepath.Join(testDataDir, "metadata", "unpack"), 1095 volumes: filepath.Join(testDataDir, "metadata", "volumes"), 1096 chroot: filepath.Join(testDataDir, "metadata", "chroot"), 1097 scratch: filepath.Join(testDataDir, "metadata", "scratch"), 1098 }, 1099 }, 1100 shouldPass: true, 1101 }, 1102 { 1103 name: "invalid format", 1104 args: args{ 1105 metadataFile: "invalid_format.json", 1106 resume: true, 1107 }, 1108 wantStateMachine: nil, 1109 shouldPass: false, 1110 expectedError: "failed to parse metadata file", 1111 }, 1112 { 1113 name: "missing state file", 1114 args: args{ 1115 metadataFile: "inexistent.json", 1116 resume: true, 1117 }, 1118 wantStateMachine: nil, 1119 shouldPass: false, 1120 expectedError: "error reading metadata file", 1121 }, 1122 { 1123 name: "do nothing if not resuming", 1124 args: args{ 1125 metadataFile: "unimportant.json", 1126 resume: false, 1127 }, 1128 wantStateMachine: &StateMachine{ 1129 stateMachineFlags: &commands.StateMachineOpts{ 1130 Resume: false, 1131 WorkDir: filepath.Join(testDataDir, "metadata"), 1132 }, 1133 states: allTestStates, 1134 }, 1135 shouldPass: true, 1136 expectedError: "error reading metadata file", 1137 }, 1138 { 1139 name: "state file with too many steps", 1140 args: args{ 1141 metadataFile: "too_many_steps.json", 1142 resume: true, 1143 }, 1144 wantStateMachine: nil, 1145 shouldPass: false, 1146 expectedError: "invalid steps taken count", 1147 }, 1148 } 1149 for _, tc := range testCases { 1150 t.Run(tc.name, func(t *testing.T) { 1151 asserter := helper.Asserter{T: t} 1152 gotStateMachine := &StateMachine{ 1153 stateMachineFlags: &commands.StateMachineOpts{ 1154 Resume: tc.args.resume, 1155 WorkDir: filepath.Join(testDataDir, "metadata"), 1156 }, 1157 states: allTestStates, 1158 } 1159 1160 err := gotStateMachine.readMetadata(tc.args.metadataFile) 1161 if tc.shouldPass { 1162 asserter.AssertErrNil(err, true) 1163 asserter.AssertEqual(tc.wantStateMachine, gotStateMachine, cmpOpts...) 1164 } else { 1165 asserter.AssertErrContains(err, tc.expectedError) 1166 } 1167 }) 1168 } 1169 } 1170 1171 func TestStateMachine_writeMetadata(t *testing.T) { 1172 tests := []struct { 1173 name string 1174 stateMachine *StateMachine 1175 shouldPass bool 1176 expectedError string 1177 }{ 1178 { 1179 name: "successful write", 1180 stateMachine: &StateMachine{ 1181 stateMachineFlags: &commands.StateMachineOpts{ 1182 Resume: true, 1183 WorkDir: filepath.Join(testDataDir, "metadata"), 1184 }, 1185 CurrentStep: "", 1186 StepsTaken: 2, 1187 YamlFilePath: "/tmp/ubuntu-image-2329554237/unpack/gadget/meta/gadget.yaml", 1188 IsSeeded: true, 1189 SectorSize: quantity.Size(512), 1190 RootfsSize: quantity.Size(775915520), 1191 states: allTestStates[2:], 1192 GadgetInfo: &gadget.Info{ 1193 Volumes: map[string]*gadget.Volume{ 1194 "pc": { 1195 Schema: "gpt", 1196 Bootloader: "grub", 1197 Structure: []gadget.VolumeStructure{ 1198 { 1199 Name: "mbr", 1200 Offset: ptrToOffset(quantity.Offset(quantity.Size(0))), 1201 MinSize: quantity.Size(440), 1202 Size: quantity.Size(440), 1203 Role: "mbr", 1204 Type: "mbr", 1205 Content: []gadget.VolumeContent{ 1206 { 1207 Image: "pc-boot.img", 1208 }, 1209 }, 1210 Update: gadget.VolumeUpdate{ 1211 Edition: 1, 1212 }, 1213 YamlIndex: 0, 1214 }, 1215 }, 1216 }, 1217 }, 1218 }, 1219 Packages: nil, 1220 Snaps: nil, 1221 ImageSizes: map[string]quantity.Size{"pc": 3155165184}, 1222 VolumeOrder: []string{"pc"}, 1223 VolumeNames: map[string]string{"pc": "pc.img"}, 1224 tempDirs: temporaryDirectories{ 1225 rootfs: filepath.Join(testDataDir, "metadata", "root"), 1226 unpack: filepath.Join(testDataDir, "metadata", "unpack"), 1227 volumes: filepath.Join(testDataDir, "metadata", "volumes"), 1228 chroot: filepath.Join(testDataDir, "metadata", "chroot"), 1229 scratch: filepath.Join(testDataDir, "metadata", "scratch"), 1230 }, 1231 }, 1232 shouldPass: true, 1233 }, 1234 { 1235 name: "fail to marshall an invalid stateMachine - use a GadgetInfo with a channel", 1236 stateMachine: &StateMachine{ 1237 stateMachineFlags: &commands.StateMachineOpts{ 1238 Resume: true, 1239 WorkDir: filepath.Join(testDataDir, "metadata"), 1240 }, 1241 GadgetInfo: &gadget.Info{ 1242 Defaults: map[string]map[string]interface{}{ 1243 "key": { 1244 "key": make(chan int), 1245 }, 1246 }, 1247 }, 1248 CurrentStep: "", 1249 StepsTaken: 2, 1250 YamlFilePath: "/tmp/ubuntu-image-2329554237/unpack/gadget/meta/gadget.yaml", 1251 }, 1252 shouldPass: false, 1253 expectedError: "failed to JSON encode metadata", 1254 }, 1255 { 1256 name: "fail to write in inexistent directory", 1257 stateMachine: &StateMachine{ 1258 stateMachineFlags: &commands.StateMachineOpts{ 1259 Resume: true, 1260 WorkDir: filepath.Join("non-existent", "metadata"), 1261 }, 1262 CurrentStep: "", 1263 StepsTaken: 2, 1264 YamlFilePath: "/tmp/ubuntu-image-2329554237/unpack/gadget/meta/gadget.yaml", 1265 }, 1266 shouldPass: false, 1267 expectedError: "error opening JSON metadata file for writing", 1268 }, 1269 } 1270 for _, tc := range tests { 1271 t.Run(tc.name, func(t *testing.T) { 1272 asserter := helper.Asserter{T: t} 1273 tName := strings.ReplaceAll(tc.name, " ", "_") 1274 err := tc.stateMachine.writeMetadata(fmt.Sprintf("result_%s.json", tName)) 1275 1276 if tc.shouldPass { 1277 want, err := os.ReadFile(filepath.Join(testDataDir, "metadata", fmt.Sprintf("reference_%s.json", tName))) 1278 if err != nil { 1279 t.Fatal("Unable to load reference metadata file: %w", err) 1280 } 1281 1282 got, err := os.ReadFile(filepath.Join(testDataDir, "metadata", fmt.Sprintf("result_%s.json", tName))) 1283 if err != nil { 1284 t.Fatal("Unable to load metadata file: %w", err) 1285 } 1286 1287 asserter.AssertEqual(want, got) 1288 1289 } else { 1290 asserter.AssertErrContains(err, tc.expectedError) 1291 } 1292 1293 }) 1294 } 1295 } 1296 1297 func TestMinSize(t *testing.T) { 1298 asserter := helper.Asserter{T: t} 1299 var stateMachine StateMachine 1300 stateMachine.commonFlags, stateMachine.stateMachineFlags = helper.InitCommonOpts() 1301 1302 stateMachine.YamlFilePath = filepath.Join("testdata", "gadget-gpt-minsize.yaml") 1303 1304 err := stateMachine.makeTemporaryDirectories() 1305 asserter.AssertErrNil(err, false) 1306 err = stateMachine.loadGadgetYaml() 1307 asserter.AssertErrNil(err, false) 1308 } 1309 1310 func TestStateMachine_displayStates(t *testing.T) { 1311 asserter := helper.Asserter{T: t} 1312 type fields struct { 1313 commonFlags *commands.CommonOpts 1314 stateMachineFlags *commands.StateMachineOpts 1315 states []stateFunc 1316 } 1317 tests := []struct { 1318 name string 1319 fields fields 1320 wantOutput string 1321 }{ 1322 { 1323 name: "simple case with 2 states", 1324 fields: fields{ 1325 commonFlags: &commands.CommonOpts{ 1326 Debug: true, 1327 }, 1328 stateMachineFlags: &commands.StateMachineOpts{}, 1329 states: []stateFunc{ 1330 { 1331 name: "stateFunc1", 1332 }, 1333 { 1334 name: "stateFunc2", 1335 }, 1336 }, 1337 }, 1338 wantOutput: ` 1339 Following states will be executed: 1340 [0] stateFunc1 1341 [1] stateFunc2 1342 1343 Continuing 1344 `, 1345 }, 1346 { 1347 name: "simple case with 2 states in dry-run mode", 1348 fields: fields{ 1349 commonFlags: &commands.CommonOpts{ 1350 Debug: false, 1351 DryRun: true, 1352 }, 1353 stateMachineFlags: &commands.StateMachineOpts{}, 1354 states: []stateFunc{ 1355 { 1356 name: "stateFunc1", 1357 }, 1358 { 1359 name: "stateFunc2", 1360 }, 1361 }, 1362 }, 1363 wantOutput: ` 1364 Following states would be executed: 1365 [0] stateFunc1 1366 [1] stateFunc2 1367 `, 1368 }, 1369 { 1370 name: "simple case with 2 states in dry-run mode and debug", 1371 fields: fields{ 1372 commonFlags: &commands.CommonOpts{ 1373 Debug: true, 1374 DryRun: true, 1375 }, 1376 stateMachineFlags: &commands.StateMachineOpts{}, 1377 states: []stateFunc{ 1378 { 1379 name: "stateFunc1", 1380 }, 1381 { 1382 name: "stateFunc2", 1383 }, 1384 }, 1385 }, 1386 wantOutput: ` 1387 Following states would be executed: 1388 [0] stateFunc1 1389 [1] stateFunc2 1390 `, 1391 }, 1392 { 1393 name: "3 states with until", 1394 fields: fields{ 1395 commonFlags: &commands.CommonOpts{ 1396 Debug: true, 1397 }, 1398 stateMachineFlags: &commands.StateMachineOpts{ 1399 Until: "stateFunc3", 1400 }, 1401 states: []stateFunc{ 1402 { 1403 name: "stateFunc1", 1404 }, 1405 { 1406 name: "stateFunc2", 1407 }, 1408 { 1409 name: "stateFunc3", 1410 }, 1411 }, 1412 }, 1413 wantOutput: ` 1414 Following states will be executed: 1415 [0] stateFunc1 1416 [1] stateFunc2 1417 1418 Continuing 1419 `, 1420 }, 1421 { 1422 name: "3 states with thru", 1423 fields: fields{ 1424 commonFlags: &commands.CommonOpts{ 1425 Debug: true, 1426 }, 1427 stateMachineFlags: &commands.StateMachineOpts{ 1428 Thru: "stateFunc2", 1429 }, 1430 states: []stateFunc{ 1431 { 1432 name: "stateFunc1", 1433 }, 1434 { 1435 name: "stateFunc2", 1436 }, 1437 { 1438 name: "stateFunc3", 1439 }, 1440 }, 1441 }, 1442 wantOutput: ` 1443 Following states will be executed: 1444 [0] stateFunc1 1445 [1] stateFunc2 1446 1447 Continuing 1448 `, 1449 }, 1450 { 1451 name: "3 states without debug", 1452 fields: fields{ 1453 commonFlags: &commands.CommonOpts{ 1454 Debug: false, 1455 }, 1456 stateMachineFlags: &commands.StateMachineOpts{ 1457 Thru: "stateFunc2", 1458 }, 1459 states: []stateFunc{ 1460 { 1461 name: "stateFunc1", 1462 }, 1463 { 1464 name: "stateFunc2", 1465 }, 1466 { 1467 name: "stateFunc3", 1468 }, 1469 }, 1470 }, 1471 wantOutput: "", 1472 }, 1473 } 1474 for _, tt := range tests { 1475 t.Run(tt.name, func(t *testing.T) { 1476 // capture stdout, calculate the states, and ensure they were printed 1477 stdout, restoreStdout, err := helper.CaptureStd(&os.Stdout) 1478 defer restoreStdout() 1479 asserter.AssertErrNil(err, true) 1480 1481 s := &StateMachine{ 1482 commonFlags: tt.fields.commonFlags, 1483 stateMachineFlags: tt.fields.stateMachineFlags, 1484 states: tt.fields.states, 1485 } 1486 s.displayStates() 1487 1488 restoreStdout() 1489 readStdout, err := io.ReadAll(stdout) 1490 asserter.AssertErrNil(err, true) 1491 1492 asserter.AssertEqual(tt.wantOutput, string(readStdout)) 1493 }) 1494 } 1495 } 1496 1497 // TestMakeTemporaryDirectories tests a successful execution of the 1498 // make_temporary_directories state with and without --workdir 1499 func TestMakeTemporaryDirectories(t *testing.T) { 1500 testCases := []struct { 1501 name string 1502 workdir string 1503 }{ 1504 {"with_workdir", "/tmp/make_temporary_directories-" + uuid.NewString()}, 1505 {"without_workdir", ""}, 1506 } 1507 for _, tc := range testCases { 1508 t.Run(tc.name, func(t *testing.T) { 1509 asserter := helper.Asserter{T: t} 1510 var stateMachine StateMachine 1511 stateMachine.commonFlags, stateMachine.stateMachineFlags = helper.InitCommonOpts() 1512 stateMachine.stateMachineFlags.WorkDir = tc.workdir 1513 err := stateMachine.makeTemporaryDirectories() 1514 asserter.AssertErrNil(err, true) 1515 1516 // make sure workdir was successfully created 1517 if _, err := os.Stat(stateMachine.stateMachineFlags.WorkDir); err != nil { 1518 t.Errorf("Failed to create workdir %s", 1519 stateMachine.stateMachineFlags.WorkDir) 1520 } 1521 os.RemoveAll(stateMachine.stateMachineFlags.WorkDir) 1522 }) 1523 } 1524 } 1525 1526 // TestFailedMakeTemporaryDirectories tests some failed executions of the make_temporary_directories state 1527 func TestFailedMakeTemporaryDirectories(t *testing.T) { 1528 asserter := helper.Asserter{T: t} 1529 var stateMachine StateMachine 1530 stateMachine.commonFlags, stateMachine.stateMachineFlags = helper.InitCommonOpts() 1531 1532 // mock os.Mkdir and test with and without a WorkDir 1533 osMkdir = mockMkdir 1534 defer func() { 1535 osMkdir = os.Mkdir 1536 }() 1537 err := stateMachine.makeTemporaryDirectories() 1538 asserter.AssertErrContains(err, "Failed to create temporary directory") 1539 1540 stateMachine.stateMachineFlags.WorkDir = testDir 1541 err = stateMachine.makeTemporaryDirectories() 1542 asserter.AssertErrContains(err, "Error creating temporary directory") 1543 1544 // mock os.MkdirAll and only test with a WorkDir 1545 osMkdirAll = mockMkdirAll 1546 defer func() { 1547 osMkdirAll = os.MkdirAll 1548 }() 1549 err = stateMachine.makeTemporaryDirectories() 1550 if err == nil { 1551 // try adding a workdir to see if that triggers the failure 1552 stateMachine.stateMachineFlags.WorkDir = testDir 1553 err = stateMachine.makeTemporaryDirectories() 1554 asserter.AssertErrContains(err, "Error creating temporary directory") 1555 } 1556 os.RemoveAll(stateMachine.stateMachineFlags.WorkDir) 1557 } 1558 1559 // TestDetermineOutputDirectory unit tests the determineOutputDirectory function 1560 func TestDetermineOutputDirectory(t *testing.T) { 1561 testDir1 := "/tmp/determine_output_dir-" + uuid.NewString() 1562 testDir2 := "/tmp/determine_output_dir-" + uuid.NewString() 1563 cwd, _ := os.Getwd() // nolint: errcheck 1564 testCases := []struct { 1565 name string 1566 workDir string 1567 outputDir string 1568 expectedOutputDir string 1569 cleanUp bool 1570 }{ 1571 {"no_workdir_no_outputdir", "", "", cwd, false}, 1572 {"yes_workdir_no_outputdir", testDir1, "", testDir1, true}, 1573 {"no_workdir_yes_outputdir", "", testDir1, testDir1, true}, 1574 {"different_workdir_and_outputdir", testDir1, testDir2, testDir2, true}, 1575 {"same_workdir_and_outputdir", testDir1, testDir1, testDir1, true}, 1576 } 1577 for _, tc := range testCases { 1578 t.Run(tc.name, func(t *testing.T) { 1579 asserter := helper.Asserter{T: t} 1580 var stateMachine StateMachine 1581 stateMachine.commonFlags, stateMachine.stateMachineFlags = helper.InitCommonOpts() 1582 stateMachine.stateMachineFlags.WorkDir = tc.workDir 1583 stateMachine.commonFlags.OutputDir = tc.outputDir 1584 1585 err := stateMachine.makeTemporaryDirectories() 1586 asserter.AssertErrNil(err, true) 1587 1588 err = stateMachine.determineOutputDirectory() 1589 asserter.AssertErrNil(err, true) 1590 if tc.cleanUp { 1591 t.Cleanup(func() { os.RemoveAll(stateMachine.commonFlags.OutputDir) }) 1592 } 1593 1594 // ensure the correct output dir was set and that it exists 1595 if stateMachine.commonFlags.OutputDir != tc.expectedOutputDir { 1596 t.Errorf("OutputDir set in in struct \"%s\" does not match expected value \"%s\"", 1597 stateMachine.commonFlags.OutputDir, tc.expectedOutputDir) 1598 } 1599 if _, err := os.Stat(stateMachine.commonFlags.OutputDir); err != nil { 1600 t.Errorf("Failed to create output directory %s", 1601 stateMachine.stateMachineFlags.WorkDir) 1602 } 1603 }) 1604 } 1605 } 1606 1607 // TestDetermineOutputDirectory_fail tests failures in the determineOutputDirectory function 1608 func TestDetermineOutputDirectory_fail(t *testing.T) { 1609 asserter := helper.Asserter{T: t} 1610 var stateMachine StateMachine 1611 stateMachine.commonFlags, stateMachine.stateMachineFlags = helper.InitCommonOpts() 1612 stateMachine.commonFlags.OutputDir = "testdir" 1613 1614 // mock os.MkdirAll 1615 osMkdirAll = mockMkdirAll 1616 defer func() { 1617 osMkdirAll = os.MkdirAll 1618 }() 1619 err := stateMachine.determineOutputDirectory() 1620 asserter.AssertErrContains(err, "Error creating OutputDir") 1621 osMkdirAll = os.MkdirAll 1622 }