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  }