diff --git a/.github/workflows/documentation.yml b/.github/workflows/documentation.yml index 4b37efa4..fb992d7f 100644 --- a/.github/workflows/documentation.yml +++ b/.github/workflows/documentation.yml @@ -7,20 +7,20 @@ jobs: runs-on: ubuntu-latest name: documentation-check steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: fetch-depth: 0 - name: Get info on if image definition has changed id: changed-files-image-def - uses: tj-actions/changed-files@v32 + uses: tj-actions/changed-files@v39 with: files: | internal/imagedefinition/image_definition.go - name: Get info on if image definition README was updated id: changed-files-image-def-readme - uses: tj-actions/changed-files@v32 + uses: tj-actions/changed-files@v39 with: files: | internal/imagedefinition/README.rst @@ -30,24 +30,24 @@ jobs: run: | echo "Struct has changed" - name: test struct README - if: steps.changed-files-image-def-reamde.outputs.any_changed != 'true' + if: steps.changed-files-image-def-readme.outputs.any_changed != 'true' run: | echo "README has not changed" - name: Fail if image definition README has not been updated - if: steps.changed-files-image-def.outputs.any_changed == 'true' && steps.changed-files-image-def-readme.outputs.any_changed != true + if: steps.changed-files-image-def.outputs.any_changed == 'true' && steps.changed-files-image-def-readme.outputs.any_changed != 'true' run: | echo "Image Definition struct has changed but README was not updated" exit 1 - name: Get info on if command options or flags have changed id: changed-files-flags - uses: tj-actions/changed-files@v32 + uses: tj-actions/changed-files@v39 with: files: | internal/commands/* - name: Fail if command line args have changed but manpage has not been updated - if: steps.changed-files-flags.outputs.any_changed == 'true' && contains(steps.changed-files-flags.outputs.changed_files, 'ubuntu-image.rst') != true + if: steps.changed-files-flags.outputs.any_changed == 'true' && contains(steps.changed-files-flags.outputs.changed_files, 'ubuntu-image.rst') != 'true' run: | echo "Command line flags have been updated but the manpage has not" exit 1 diff --git a/cmd/ubuntu-image/main.go b/cmd/ubuntu-image/main.go index b77005a0..05afa50a 100644 --- a/cmd/ubuntu-image/main.go +++ b/cmd/ubuntu-image/main.go @@ -71,6 +71,11 @@ func executeStateMachine(sm statemachine.SmInterface) error { // unhidePackOpts make pack options visible in help if the pack command is used // This should be removed when the pack command is made visible to everyone func unhidePackOpts(parser *flags.Parser) { + // Save given options before removing them temporarily + // otherwise the help will be displayed twice + opts := parser.Options + parser.Options = 0 + defer func() { parser.Options = opts }() // parse once to determine the active command // we do not care about error here since we will reparse again _, _ = parser.Parse() diff --git a/debian/changelog b/debian/changelog index 07add5f8..922d981d 100644 --- a/debian/changelog +++ b/debian/changelog @@ -33,6 +33,7 @@ ubuntu-image (3.0+23.04ubuntu1) UNRELEASED; urgency=medium * Add experimental ubuntu-image 'pack' support * Support the keep-enabled parameter in ubuntu-image extra-ppas * Clean image build leftovers + * Make sure the fstab file is cleanly overridden (LP: #2031889) [ Alfonso Sanchez-Beato ] * Update to latest snapd, adapting to changes in layouts code. diff --git a/internal/helper/helper.go b/internal/helper/helper.go index aed21afa..e44ee41c 100644 --- a/internal/helper/helper.go +++ b/internal/helper/helper.go @@ -135,14 +135,16 @@ func SetDefaults(needsDefaults interface{}) error { } // special case for pointer to bools } else if field.Type().Elem() == reflect.TypeOf(true) { - // make sure the pointer is never nil in case no value - // was set - if field.IsNil() { - field.Set(reflect.ValueOf(BoolPtr(false))) + // if a value is set, do nothing + if !field.IsNil() { + continue } tags := elem.Type().Field(i).Tag defaultValue, hasDefault := tags.Lookup("default") if !hasDefault { + // If no default and no value is set, make sure we have a valid + // value consistent with the "zero" value for a bool (false) + field.Set(reflect.ValueOf(BoolPtr(false))) continue } if defaultValue == "true" { diff --git a/internal/helper/helper_test.go b/internal/helper/helper_test.go index 58532fa9..6dc6e721 100644 --- a/internal/helper/helper_test.go +++ b/internal/helper/helper_test.go @@ -230,7 +230,7 @@ func TestSetDefaults(t *testing.T) { }, want: &S2{ A: "test", - B: BoolPtr(true), + B: BoolPtr(false), C: BoolPtr(false), D: BoolPtr(true), }, diff --git a/internal/imagedefinition/README.rst b/internal/imagedefinition/README.rst index db35ed91..44da978d 100644 --- a/internal/imagedefinition/README.rst +++ b/internal/imagedefinition/README.rst @@ -242,6 +242,7 @@ The following specification defines what is supported in the YAML: # ubuntu-image will support creating many different types of # artifacts, including the actual images, manifest files, # changelogs, and a list of files in the rootfs. + # Set a custom fstab. The existing one (if any) will be truncated. fstab: (optional) - # the value of LABEL= for the fstab entry diff --git a/internal/statemachine/classic_states.go b/internal/statemachine/classic_states.go index 6eaa65d0..e0ae9719 100644 --- a/internal/statemachine/classic_states.go +++ b/internal/statemachine/classic_states.go @@ -27,8 +27,10 @@ import ( "github.com/canonical/ubuntu-image/internal/imagedefinition" ) -var seedVersionRegex = regexp.MustCompile(`^[a-z0-9].*`) -var localePresentRegex = regexp.MustCompile(`(?m)^LANG=|LC_[A-Z_]+=`) +var ( + seedVersionRegex = regexp.MustCompile(`^[a-z0-9].*`) + localePresentRegex = regexp.MustCompile(`(?m)^LANG=|LC_[A-Z_]+=`) +) // parseImageDefinition parses the provided yaml file and ensures it is valid func (stateMachine *StateMachine) parseImageDefinition() error { @@ -1014,9 +1016,9 @@ func (stateMachine *StateMachine) customizeCloudInit() error { func (stateMachine *StateMachine) customizeFstab() error { classicStateMachine := stateMachine.parent.(*ClassicStateMachine) - // open /etc/fstab for writing - fstabIO, err := osOpenFile(filepath.Join(stateMachine.tempDirs.chroot, "etc", "fstab"), - os.O_CREATE|os.O_WRONLY, 0644) + fstabPath := filepath.Join(stateMachine.tempDirs.chroot, "etc", "fstab") + + fstabIO, err := osOpenFile(fstabPath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0644) if err != nil { return fmt.Errorf("Error opening fstab: %s", err.Error()) } @@ -1040,6 +1042,7 @@ func (stateMachine *StateMachine) customizeFstab() error { ) fstabEntries = append(fstabEntries, fstabEntry) } + _, err = fstabIO.Write([]byte(strings.Join(fstabEntries, "\n") + "\n")) return err @@ -1289,24 +1292,68 @@ func (stateMachine *StateMachine) populateClassicRootfsContents() error { } } - if classicStateMachine.ImageDef.Customization != nil { - if len(classicStateMachine.ImageDef.Customization.Fstab) == 0 { - fstabPath := filepath.Join(classicStateMachine.tempDirs.rootfs, "etc", "fstab") - fstabBytes, err := osReadFile(fstabPath) - if err == nil { - if !strings.Contains(string(fstabBytes), "LABEL=writable") { - re := regexp.MustCompile(`(?m:^LABEL=\S+\s+/\s+(.*)$)`) - newContents := re.ReplaceAll(fstabBytes, []byte("LABEL=writable\t/\t$1")) - if !strings.Contains(string(newContents), "LABEL=writable") { - newContents = []byte("LABEL=writable / ext4 defaults 0 0\n") - } - err := osWriteFile(fstabPath, newContents, 0644) - if err != nil { - return fmt.Errorf("Error writing to fstab: %s", err.Error()) - } - } - } + if classicStateMachine.ImageDef.Customization == nil { + return nil + } + + return classicStateMachine.fixFstab() +} + +// fixFstab makes sure the fstab contains a valid entry for the root mount point +func (stateMachine *StateMachine) fixFstab() error { + classicStateMachine := stateMachine.parent.(*ClassicStateMachine) + + if len(classicStateMachine.ImageDef.Customization.Fstab) != 0 { + return nil + } + + fstabPath := filepath.Join(classicStateMachine.tempDirs.rootfs, "etc", "fstab") + fstabBytes, err := osReadFile(fstabPath) + if err != nil { + return fmt.Errorf("Error reading fstab: %s", err.Error()) + } + + rootMountFound := false + newLines := make([]string, 0) + rootFSLabel := "writable" + rootFSOptions := "discard,errors=remount-ro" + fsckOrder := "1" + + lines := strings.Split(string(fstabBytes), "\n") + for _, l := range lines { + if l == "# UNCONFIGURED FSTAB" { + // omit this line if still present + continue + } + + if strings.HasPrefix(l, "#") { + newLines = append(newLines, l) + continue + } + + entry := strings.Fields(l) + if len(entry) < 6 { + // ignore invalid fstab entry + continue } + + if entry[1] == "/" && !rootMountFound { + entry[0] = "LABEL=" + rootFSLabel + entry[3] = rootFSOptions + entry[5] = fsckOrder + + rootMountFound = true + } + newLines = append(newLines, strings.Join(entry, "\t")) + } + + if !rootMountFound { + newLines = append(newLines, fmt.Sprintf("LABEL=%s / ext4 %s 0 %s", rootFSLabel, rootFSOptions, fsckOrder)) + } + + err = osWriteFile(fstabPath, []byte(strings.Join(newLines, "\n")+"\n"), 0644) + if err != nil { + return fmt.Errorf("Error writing to fstab: %s", err.Error()) } return nil } diff --git a/internal/statemachine/classic_test.go b/internal/statemachine/classic_test.go index 076c1a96..cffe4da0 100644 --- a/internal/statemachine/classic_test.go +++ b/internal/statemachine/classic_test.go @@ -7,7 +7,6 @@ import ( "errors" "fmt" "io" - "io/fs" "os" "os/exec" "path" @@ -1855,9 +1854,9 @@ func TestFailedPrepareClassicImage(t *testing.T) { }) } -// TestPopulateClassicRootfsContents runs the state machine through populate_rootfs_contents and examines +// TestStateMachine_PopulateClassicRootfsContents runs the state machine through populate_rootfs_contents and examines // the rootfs to ensure at least some of the correct file are in place -func TestPopulateClassicRootfsContents(t *testing.T) { +func TestStateMachine_PopulateClassicRootfsContents(t *testing.T) { t.Run("test_populate_classic_rootfs_contents", func(t *testing.T) { if runtime.GOARCH != "amd64" { t.Skip("Test for amd64 only") @@ -1875,6 +1874,7 @@ func TestPopulateClassicRootfsContents(t *testing.T) { Rootfs: &imagedefinition.Rootfs{ Archive: "ubuntu", }, + Customization: &imagedefinition.Customization{}, } // need workdir set up for this @@ -1901,13 +1901,34 @@ func TestPopulateClassicRootfsContents(t *testing.T) { } } + // return when Customization.Fstab is not empty + stateMachine.ImageDef.Customization.Fstab = []*imagedefinition.Fstab{ + { + Label: "writable", + Mountpoint: "/", + FSType: "ext4", + MountOptions: "defaults", + Dump: true, + FsckOrder: 1, + }, + } + + err = stateMachine.populateClassicRootfsContents() + asserter.AssertErrNil(err, true) + + // return when no Customization + stateMachine.ImageDef.Customization = nil + + err = stateMachine.populateClassicRootfsContents() + asserter.AssertErrNil(err, true) + os.RemoveAll(stateMachine.stateMachineFlags.WorkDir) }) } -// TestFailedPopulateClassicRootfsContents tests failed scenarios in populateClassicRootfsContents +// TestStateMachine_FailedPopulateClassicRootfsContents tests failed scenarios in populateClassicRootfsContents // this is accomplished by mocking functions -func TestFailedPopulateClassicRootfsContents(t *testing.T) { +func TestStateMachine_FailedPopulateClassicRootfsContents(t *testing.T) { t.Run("test_failed_populate_classic_rootfs_contents", func(t *testing.T) { asserter := helper.Asserter{T: t} var stateMachine ClassicStateMachine @@ -1959,6 +1980,24 @@ func TestFailedPopulateClassicRootfsContents(t *testing.T) { asserter.AssertErrContains(err, "Error writing to fstab") osWriteFile = os.WriteFile + // mock os.ReadFile + osReadFile = mockReadFile + defer func() { + osReadFile = os.ReadFile + }() + err = stateMachine.populateClassicRootfsContents() + asserter.AssertErrContains(err, "Error reading fstab") + osReadFile = os.ReadFile + + // return when existing fstab contains LABEL=writable + //nolint:gosec,G306 + err = os.WriteFile(filepath.Join(stateMachine.tempDirs.chroot, "etc", "fstab"), + []byte("LABEL=writable\n"), + 0644) + asserter.AssertErrNil(err, true) + err = stateMachine.populateClassicRootfsContents() + asserter.AssertErrNil(err, true) + // create an /etc/resolv.conf.tmp in the chroot err = os.MkdirAll(filepath.Join(stateMachine.tempDirs.chroot, "etc"), 0755) asserter.AssertErrNil(err, true) @@ -1976,6 +2015,106 @@ func TestFailedPopulateClassicRootfsContents(t *testing.T) { }) } +// TestSateMachine_fixFstab tests functionality of the fixFstab function +func TestSateMachine_fixFstab(t *testing.T) { + testCases := []struct { + name string + existingFstab string + expectedFstab string + }{ + { + name: "add entry to an existing but empty fstab", + existingFstab: "# UNCONFIGURED FSTAB", + expectedFstab: `LABEL=writable / ext4 discard,errors=remount-ro 0 1 +`, + }, + { + name: "fix existing entry amongst several others", + existingFstab: `# /etc/fstab: static file system information. +UUID=1565-1398 / ext4 defaults 0 0 +#Here is another comment that should be left in place +/dev/mapper/vgubuntu-swap_1 none swap sw 0 0 +`, + expectedFstab: `# /etc/fstab: static file system information. +LABEL=writable / ext4 discard,errors=remount-ro 0 1 +#Here is another comment that should be left in place +/dev/mapper/vgubuntu-swap_1 none swap sw 0 0 +`, + }, + { + name: "fix existing entry amongst several others (with spaces)", + existingFstab: `# /etc/fstab: static file system information. +UUID=1565-1398 / ext4 defaults 0 0 +/dev/mapper/vgubuntu-swap_1 none swap sw 0 0 +`, + expectedFstab: `# /etc/fstab: static file system information. +LABEL=writable / ext4 discard,errors=remount-ro 0 1 +/dev/mapper/vgubuntu-swap_1 none swap sw 0 0 +`, + }, + { + name: "fix only one root mount point", + existingFstab: `# /etc/fstab: static file system information. +UUID=1565-1398 / ext4 defaults 0 0 +UUID=1234-5678 / ext4 defaults 0 0 +`, + expectedFstab: `# /etc/fstab: static file system information. +LABEL=writable / ext4 discard,errors=remount-ro 0 1 +UUID=1234-5678 / ext4 defaults 0 0 +`, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + asserter := helper.Asserter{T: t} + saveCWD := helper.SaveCWD() + defer saveCWD() + + var stateMachine ClassicStateMachine + stateMachine.commonFlags, stateMachine.stateMachineFlags = helper.InitCommonOpts() + stateMachine.parent = &stateMachine + stateMachine.ImageDef = imagedefinition.ImageDefinition{ + Architecture: getHostArch(), + Series: getHostSuite(), + Rootfs: &imagedefinition.Rootfs{}, + Customization: &imagedefinition.Customization{}, + } + + // set the defaults for the imageDef + err := helper.SetDefaults(&stateMachine.ImageDef) + asserter.AssertErrNil(err, true) + + // need workdir set up for this + err = stateMachine.makeTemporaryDirectories() + asserter.AssertErrNil(err, true) + + // create the /etc directory + err = os.MkdirAll(filepath.Join(stateMachine.tempDirs.rootfs, "etc"), 0644) + asserter.AssertErrNil(err, true) + + fstabPath := filepath.Join(stateMachine.tempDirs.rootfs, "etc", "fstab") + + // simulate an already existing fstab file + if len(tc.existingFstab) != 0 { + err = osWriteFile(fstabPath, []byte(tc.existingFstab), 0644) + asserter.AssertErrNil(err, true) + } + + err = stateMachine.fixFstab() + asserter.AssertErrNil(err, true) + + fstabBytes, err := os.ReadFile(fstabPath) + asserter.AssertErrNil(err, true) + + if string(fstabBytes) != tc.expectedFstab { + t.Errorf("Expected fstab content \"%s\", but got \"%s\"", + tc.expectedFstab, string(fstabBytes)) + } + }) + } +} + // TestGeneratePackageManifest tests if classic image manifest generation works func TestGeneratePackageManifest(t *testing.T) { t.Run("test_generate_package_manifest", func(t *testing.T) { @@ -3602,10 +3741,26 @@ func TestCustomizeFstab(t *testing.T) { name string fstab []*imagedefinition.Fstab expectedFstab string + existingFstab string }{ { - "one_entry", - []*imagedefinition.Fstab{ + name: "one entry to an empty fstab", + fstab: []*imagedefinition.Fstab{ + { + Label: "writable", + Mountpoint: "/", + FSType: "ext4", + MountOptions: "defaults", + Dump: true, + FsckOrder: 1, + }, + }, + expectedFstab: `LABEL=writable / ext4 defaults 1 1 +`, + }, + { + name: "one entry to a non-empty fstab", + fstab: []*imagedefinition.Fstab{ { Label: "writable", Mountpoint: "/", @@ -3615,12 +3770,13 @@ func TestCustomizeFstab(t *testing.T) { FsckOrder: 1, }, }, - `LABEL=writable / ext4 defaults 1 1 + expectedFstab: `LABEL=writable / ext4 defaults 1 1 `, + existingFstab: `LABEL=xxx / ext4 discard,errors=remount-ro 0 1`, }, { - "two_entries", - []*imagedefinition.Fstab{ + name: "two entries", + fstab: []*imagedefinition.Fstab{ { Label: "writable", Mountpoint: "/", @@ -3638,21 +3794,8 @@ func TestCustomizeFstab(t *testing.T) { FsckOrder: 1, }, }, - `LABEL=writable / ext4 defaults 0 1 + expectedFstab: `LABEL=writable / ext4 defaults 0 1 LABEL=system-boot /boot/firmware vfat defaults 0 1 -`, - }, - { - "defaults_assumed", - []*imagedefinition.Fstab{ - { - Label: "writable", - Mountpoint: "/", - FSType: "ext4", - FsckOrder: 1, - }, - }, - `LABEL=writable / ext4 defaults 0 1 `, }, } @@ -3687,13 +3830,19 @@ LABEL=system-boot /boot/firmware vfat defaults 0 1 err = os.MkdirAll(filepath.Join(stateMachine.tempDirs.chroot, "etc"), 0644) asserter.AssertErrNil(err, true) + fstabPath := filepath.Join(stateMachine.tempDirs.chroot, "etc", "fstab") + + // simulate an already existing fstab file + if len(tc.existingFstab) != 0 { + err = osWriteFile(fstabPath, []byte(tc.existingFstab), 0644) + asserter.AssertErrNil(err, true) + } + // customize the fstab, ensure no errors, and check the contents err = stateMachine.customizeFstab() asserter.AssertErrNil(err, true) - fstabBytes, err := os.ReadFile( - filepath.Join(stateMachine.tempDirs.chroot, "etc", "fstab"), - ) + fstabBytes, err := os.ReadFile(fstabPath) asserter.AssertErrNil(err, true) if string(fstabBytes) != tc.expectedFstab { @@ -3704,12 +3853,12 @@ LABEL=system-boot /boot/firmware vfat defaults 0 1 } } -// TestFailedCustomizeFstab tests failures in the customizeFstab function -func TestFailedCustomizeFstab(t *testing.T) { +// TestStateMachine_customizeFstab_fail tests failures in the customizeFstab function +func TestStateMachine_customizeFstab_fail(t *testing.T) { t.Run("test_failed_customize_fstab", func(t *testing.T) { asserter := helper.Asserter{T: t} - saveCWD := helper.SaveCWD() - defer saveCWD() + restoreCWD := helper.SaveCWD() + defer restoreCWD() var stateMachine ClassicStateMachine stateMachine.commonFlags, stateMachine.stateMachineFlags = helper.InitCommonOpts() @@ -3732,14 +3881,12 @@ func TestFailedCustomizeFstab(t *testing.T) { }, } - // mock os.OpenFile osOpenFile = mockOpenFile defer func() { osOpenFile = os.OpenFile }() err := stateMachine.customizeFstab() asserter.AssertErrContains(err, "Error opening fstab") - osOpenFile = os.OpenFile }) } @@ -4448,59 +4595,6 @@ func TestClassicStateMachine_cleanRootfs_real_rootfs(t *testing.T) { }) } -type osMockConf struct { - osutilCopySpecialFileThreshold uint - ReadDirThreshold uint - RemoveThreshold uint - TruncateThreshold uint -} - -type osMock struct { - conf *osMockConf - beforeOsutilCopySpecialFileFail uint - beforeReadDirFail uint - beforeRemoveFail uint - beforeTruncateFail uint -} - -func (o *osMock) CopySpecialFile(path, dest string) error { - if o.beforeOsutilCopySpecialFileFail >= o.conf.osutilCopySpecialFileThreshold { - return fmt.Errorf("CopySpecialFile fail") - } - o.beforeOsutilCopySpecialFileFail++ - return nil -} - -func (o *osMock) ReadDir(name string) ([]fs.DirEntry, error) { - if o.beforeReadDirFail >= o.conf.ReadDirThreshold { - return nil, fmt.Errorf("ReadDir fail") - } - o.beforeReadDirFail++ - return []fs.DirEntry{}, nil -} - -func (o *osMock) Remove(name string) error { - if o.beforeRemoveFail >= o.conf.RemoveThreshold { - return fmt.Errorf("Remove fail") - } - o.beforeRemoveFail++ - - return nil -} - -func (o *osMock) Truncate(name string, size int64) error { - if o.beforeTruncateFail >= o.conf.TruncateThreshold { - return fmt.Errorf("Truncate fail") - } - o.beforeTruncateFail++ - - return nil -} - -func NewOSMock(conf *osMockConf) *osMock { - return &osMock{conf: conf} -} - func TestClassicStateMachine_cleanRootfs(t *testing.T) { sampleContent := "test" sampleSize := int64(len(sampleContent)) diff --git a/internal/statemachine/helper.go b/internal/statemachine/helper.go index 282af199..2d11936f 100644 --- a/internal/statemachine/helper.go +++ b/internal/statemachine/helper.go @@ -900,6 +900,7 @@ func checkCustomizationSteps(searchStruct interface{}, tag string) (extraStates elem := value.Elem() for i := 0; i < elem.NumField(); i++ { field := elem.Field(i) + if !field.IsNil() { tags := elem.Type().Field(i).Tag tagValue, hasTag := tags.Lookup(tag) diff --git a/internal/statemachine/pack_test.go b/internal/statemachine/pack_test.go index 095d7570..bf5be955 100644 --- a/internal/statemachine/pack_test.go +++ b/internal/statemachine/pack_test.go @@ -2,7 +2,6 @@ package statemachine import ( "fmt" - "io/fs" "os" "os/exec" "path/filepath" @@ -15,50 +14,6 @@ import ( "github.com/canonical/ubuntu-image/internal/helper" ) -type osMockConf struct { - osutilCopySpecialFileThreshold uint - ReadDirThreshold uint - RemoveThreshold uint -} - -type osMock struct { - conf *osMockConf - beforeOsutilCopySpecialFileFail uint - beforeReadDirFail uint - beforeRemoveFail uint -} - -func (o *osMock) CopySpecialFile(path, dest string) error { - if o.beforeOsutilCopySpecialFileFail >= o.conf.osutilCopySpecialFileThreshold { - return fmt.Errorf("CopySpecialFile fail") - } - o.beforeOsutilCopySpecialFileFail++ - - return nil -} - -func (o *osMock) ReadDir(name string) ([]fs.DirEntry, error) { - if o.beforeReadDirFail >= o.conf.ReadDirThreshold { - return nil, fmt.Errorf("ReadDir fail") - } - o.beforeReadDirFail++ - - return []fs.DirEntry{}, nil -} - -func (o *osMock) Remove(name string) error { - if o.beforeRemoveFail >= o.conf.RemoveThreshold { - return fmt.Errorf("Remove fail") - } - o.beforeRemoveFail++ - - return nil -} - -func NewOSMock(conf *osMockConf) *osMock { - return &osMock{conf: conf} -} - func TestPack_Setup(t *testing.T) { t.Run("test_classic_setup", func(t *testing.T) { asserter := helper.Asserter{T: t} diff --git a/internal/statemachine/tests_helper_test.go b/internal/statemachine/tests_helper_test.go new file mode 100644 index 00000000..7bfdd568 --- /dev/null +++ b/internal/statemachine/tests_helper_test.go @@ -0,0 +1,65 @@ +package statemachine + +import ( + "fmt" + "io/fs" +) + +type osMockConf struct { + osutilCopySpecialFileThreshold uint + ReadDirThreshold uint + RemoveThreshold uint + TruncateThreshold uint +} + +// osMock holds methods to easily mock functions from os and snapd/osutil packages +// Each method can be configured to fail after a given number of calls +// This could be improved by letting the mock functions calls the real +// functions before failing. +type osMock struct { + conf *osMockConf + beforeOsutilCopySpecialFileFail uint + beforeReadDirFail uint + beforeRemoveFail uint + beforeTruncateFail uint +} + +func (o *osMock) CopySpecialFile(path, dest string) error { + if o.beforeOsutilCopySpecialFileFail >= o.conf.osutilCopySpecialFileThreshold { + return fmt.Errorf("CopySpecialFile fail") + } + o.beforeOsutilCopySpecialFileFail++ + + return nil +} + +func (o *osMock) ReadDir(name string) ([]fs.DirEntry, error) { + if o.beforeReadDirFail >= o.conf.ReadDirThreshold { + return nil, fmt.Errorf("ReadDir fail") + } + o.beforeReadDirFail++ + + return []fs.DirEntry{}, nil +} + +func (o *osMock) Remove(name string) error { + if o.beforeRemoveFail >= o.conf.RemoveThreshold { + return fmt.Errorf("Remove fail") + } + o.beforeRemoveFail++ + + return nil +} + +func (o *osMock) Truncate(name string, size int64) error { + if o.beforeTruncateFail >= o.conf.TruncateThreshold { + return fmt.Errorf("Truncate fail") + } + o.beforeTruncateFail++ + + return nil +} + +func NewOSMock(conf *osMockConf) *osMock { + return &osMock{conf: conf} +}