constellation/internal/file/file_test.go

640 lines
16 KiB
Go
Raw Normal View History

/*
Copyright (c) Edgeless Systems GmbH
SPDX-License-Identifier: AGPL-3.0-only
*/
package file
import (
"encoding/json"
cli: Terraform migrations on upgrade (#1685) * add terraform planning * overwrite terraform files in upgrade workspace * Revert "overwrite terraform files in upgrade workspace" This reverts commit 8bdacfb8bef23ef2cdbdb06bad0855b3bbc42df0. * prepare terraform workspace * test upgrade integration * print upgrade abort * rename plan file * write output to file * add show plan test * add upgrade tf workdir * fix workspace preparing * squash to 1 command * test * bazel build * plan test * register flag manually * bazel tidy * fix linter * remove MAA variable * fix workdir * accept tf variables * variable fetching * fix resource indices * accept Terraform targets * refactor upgrade command * Terraform migration apply unit test * pass down image fetcher to test * use new flags in e2e test * move file name to constant * update buildfiles * fix version constant * conditionally create MAA * move interface down * upgrade dir * update buildfiles * fix interface * fix createMAA check * fix imports * update buildfiles * wip: workspace backup * copy utils * backup upgrade workspace * remove debug print * replace old state after upgrade * check if flag exists * prepare test workspace * remove prefix Co-authored-by: Otto Bittner <cobittner@posteo.net> * respect file permissions * refactor tf upgrader * check workspace before upgrades * remove temp upgrade dir after completion * clean up workspace after abortion * fix upgrade apply test * fix linter --------- Co-authored-by: Otto Bittner <cobittner@posteo.net>
2023-05-22 07:31:20 -04:00
"io/fs"
"path/filepath"
"testing"
2022-09-21 07:47:57 -04:00
"github.com/edgelesssys/constellation/v2/internal/constants"
"github.com/spf13/afero"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.uber.org/goleak"
"gopkg.in/yaml.v3"
)
func TestMain(m *testing.M) {
goleak.VerifyTestMain(m)
}
func TestWrite(t *testing.T) {
testCases := map[string]struct {
fs afero.Fs
setupFs func(af afero.Afero) error
name string
content string
expectedContent string
options Option
wantErr bool
wantAppend bool
}{
"successful write": {
fs: afero.NewMemMapFs(),
content: "asdf",
expectedContent: "asdf",
name: "somedir/somefile",
},
"successful overwrite": {
fs: afero.NewMemMapFs(),
setupFs: func(af afero.Afero) error { return af.WriteFile("somedir/somefile", []byte{}, 0o644) },
content: "asdf",
expectedContent: "asdf",
name: "somedir/somefile",
options: OptOverwrite,
},
"successful append": {
fs: afero.NewMemMapFs(),
setupFs: func(af afero.Afero) error { return af.WriteFile("somedir/somefile", []byte("fdsa"), 0o644) },
content: "asdf",
expectedContent: "fdsaasdf",
name: "somedir/somefile",
options: OptAppend,
},
"read only fs": {
fs: afero.NewReadOnlyFs(afero.NewMemMapFs()),
name: "somedir/somefile",
wantErr: true,
},
"file already exists": {
fs: afero.NewMemMapFs(),
setupFs: func(af afero.Afero) error { return af.WriteFile("somedir/somefile", []byte{}, 0o644) },
name: "somedir/somefile",
wantErr: true,
},
}
for name, tc := range testCases {
t.Run(name, func(t *testing.T) {
assert := assert.New(t)
require := require.New(t)
handler := NewHandler(tc.fs)
if tc.setupFs != nil {
require.NoError(tc.setupFs(afero.Afero{Fs: tc.fs}))
}
if tc.wantErr {
assert.Error(handler.Write(tc.name, []byte(tc.content), tc.options))
} else {
assert.NoError(handler.Write(tc.name, []byte(tc.content), tc.options))
content, err := handler.Read(tc.name)
require.NoError(err)
assert.Equal(tc.expectedContent, string(content))
}
})
}
}
func TestReadJSON(t *testing.T) {
type testContent struct {
First string
Second int
}
someContent := testContent{
First: "first",
Second: 2,
}
jsonContent, err := json.MarshalIndent(someContent, "", "\t")
require.NoError(t, err)
testCases := map[string]struct {
fs afero.Fs
setupFs func(fs *afero.Afero) error
name string
wantContent any
wantErr bool
}{
"successful read": {
fs: afero.NewMemMapFs(),
name: "test/statefile",
setupFs: func(fs *afero.Afero) error { return fs.WriteFile("test/statefile", jsonContent, 0o755) },
wantContent: someContent,
},
"file not existent": {
fs: afero.NewMemMapFs(),
name: "test/statefile",
wantErr: true,
},
"file not json": {
fs: afero.NewMemMapFs(),
name: "test/statefile",
setupFs: func(fs *afero.Afero) error { return fs.WriteFile("test/statefile", []byte{0x1}, 0o755) },
wantErr: true,
},
}
for name, tc := range testCases {
t.Run(name, func(t *testing.T) {
assert := assert.New(t)
require := require.New(t)
handler := NewHandler(tc.fs)
if tc.setupFs != nil {
require.NoError(tc.setupFs(handler.fs))
}
resultContent := &testContent{}
if tc.wantErr {
assert.Error(handler.ReadJSON(tc.name, resultContent))
} else {
assert.NoError(handler.ReadJSON(tc.name, resultContent))
assert.Equal(tc.wantContent, *resultContent)
}
})
}
}
func TestWriteJSON(t *testing.T) {
type testContent struct {
First string
Second int
}
someContent := testContent{
First: "first",
Second: 2,
}
notMarshalableContent := struct{ Foo chan int }{Foo: make(chan int)}
testCases := map[string]struct {
fs afero.Fs
setupFs func(af afero.Afero) error
name string
content any
options Option
wantErr bool
}{
"successful write": {
fs: afero.NewMemMapFs(),
name: "test/statefile",
content: someContent,
},
"successful overwrite": {
fs: afero.NewMemMapFs(),
setupFs: func(af afero.Afero) error { return af.WriteFile("test/statefile", []byte{}, 0o644) },
name: "test/statefile",
content: someContent,
options: OptOverwrite,
},
"read only fs": {
fs: afero.NewReadOnlyFs(afero.NewMemMapFs()),
name: "test/statefile",
content: someContent,
wantErr: true,
},
"file already exists": {
fs: afero.NewMemMapFs(),
setupFs: func(af afero.Afero) error { return af.WriteFile("test/statefile", []byte{}, 0o644) },
name: "test/statefile",
content: someContent,
wantErr: true,
},
"marshal error": {
fs: afero.NewMemMapFs(),
name: "test/statefile",
content: notMarshalableContent,
wantErr: true,
},
"mkdirAll works": {
fs: afero.NewMemMapFs(),
name: "test/statefile",
content: someContent,
options: OptMkdirAll,
},
// TODO(malt3): add tests for mkdirAll actually creating the necessary folders when https://github.com/spf13/afero/issues/270 is fixed.
// Currently, MemMapFs will create files in nonexistent directories due to a bug in afero,
// making it impossible to test the actual behavior of the mkdirAll parameter.
}
for name, tc := range testCases {
t.Run(name, func(t *testing.T) {
assert := assert.New(t)
require := require.New(t)
handler := NewHandler(tc.fs)
if tc.setupFs != nil {
require.NoError(tc.setupFs(afero.Afero{Fs: tc.fs}))
}
if tc.wantErr {
assert.Error(handler.WriteJSON(tc.name, tc.content, tc.options))
} else {
assert.NoError(handler.WriteJSON(tc.name, tc.content, tc.options))
resultContent := &testContent{}
assert.NoError(handler.ReadJSON(tc.name, resultContent))
assert.Equal(tc.content, *resultContent)
}
})
}
}
func TestReadYAML(t *testing.T) {
type testContent struct {
First string
Second int
}
someContent := testContent{
First: "first",
Second: 2,
}
yamlContent, err := yaml.Marshal(someContent)
require.NoError(t, err)
testCases := map[string]struct {
fs afero.Fs
setupFs func(fs *afero.Afero) error
name string
wantContent any
wantErr bool
}{
"successful read": {
fs: afero.NewMemMapFs(),
name: "test/config.yaml",
setupFs: func(fs *afero.Afero) error { return fs.WriteFile("test/config.yaml", yamlContent, 0o755) },
wantContent: someContent,
},
"file not existent": {
fs: afero.NewMemMapFs(),
name: "test/config.yaml",
wantErr: true,
},
"file not yaml": {
fs: afero.NewMemMapFs(),
name: "test/config.yaml",
setupFs: func(fs *afero.Afero) error { return fs.WriteFile("test/config.yaml", []byte{0x1}, 0o755) },
wantErr: true,
},
}
for name, tc := range testCases {
t.Run(name, func(t *testing.T) {
assert := assert.New(t)
require := require.New(t)
handler := NewHandler(tc.fs)
if tc.setupFs != nil {
require.NoError(tc.setupFs(handler.fs))
}
resultContent := &testContent{}
if tc.wantErr {
assert.Error(handler.ReadYAML(tc.name, resultContent))
} else {
assert.NoError(handler.ReadYAML(tc.name, resultContent))
assert.Equal(tc.wantContent, *resultContent)
}
})
}
}
func TestReadYAMLStrictUnknownFieldFails(t *testing.T) {
assert := assert.New(t)
type SampleConfig struct {
Version string `yaml:"version"`
Value string `yaml:"value"`
}
yamlConfig := `
version: "1.0.0"
value: "foobar"
sneakyValue: "superSecret"
`
handler := NewHandler(afero.NewMemMapFs())
assert.NoError(handler.Write(constants.ConfigFilename, []byte(yamlConfig), OptNone))
var readInConfig SampleConfig
assert.Error(handler.ReadYAMLStrict(constants.ConfigFilename, &readInConfig))
}
func TestWriteYAML(t *testing.T) {
type testContent struct {
First string
Second int
}
someContent := testContent{
First: "first",
Second: 2,
}
notMarshalableContent := struct{ Foo chan int }{Foo: make(chan int)}
testCases := map[string]struct {
fs afero.Fs
setupFs func(af afero.Afero) error
name string
content any
options Option
wantErr bool
}{
"successful write": {
fs: afero.NewMemMapFs(),
name: "test/statefile",
content: someContent,
},
"successful overwrite": {
fs: afero.NewMemMapFs(),
setupFs: func(af afero.Afero) error { return af.WriteFile("test/statefile", []byte{}, 0o644) },
name: "test/statefile",
content: someContent,
options: OptOverwrite,
},
"read only fs": {
fs: afero.NewReadOnlyFs(afero.NewMemMapFs()),
name: "test/statefile",
content: someContent,
wantErr: true,
},
"file already exists": {
fs: afero.NewMemMapFs(),
setupFs: func(af afero.Afero) error { return af.WriteFile("test/statefile", []byte{}, 0o644) },
name: "test/statefile",
content: someContent,
wantErr: true,
},
"marshal error": {
fs: afero.NewMemMapFs(),
name: "test/statefile",
content: notMarshalableContent,
wantErr: true,
},
"mkdirAll works": {
fs: afero.NewMemMapFs(),
name: "test/statefile",
content: someContent,
options: OptMkdirAll,
},
// TODO(malt3): add tests for mkdirAll actually creating the necessary folders when https://github.com/spf13/afero/issues/270 is fixed.
// Currently, MemMapFs will create files in nonexistent directories due to a bug in afero,
// making it impossible to test the actual behavior of the mkdirAll parameter.
}
for name, tc := range testCases {
t.Run(name, func(t *testing.T) {
assert := assert.New(t)
require := require.New(t)
handler := NewHandler(tc.fs)
if tc.setupFs != nil {
require.NoError(tc.setupFs(afero.Afero{Fs: tc.fs}))
}
if tc.wantErr {
assert.Error(handler.WriteYAML(tc.name, tc.content, tc.options))
} else {
assert.NoError(handler.WriteYAML(tc.name, tc.content, tc.options))
resultContent := &testContent{}
assert.NoError(handler.ReadYAML(tc.name, resultContent))
assert.Equal(tc.content, *resultContent)
}
})
}
}
func TestRemove(t *testing.T) {
assert := assert.New(t)
require := require.New(t)
fs := afero.NewMemMapFs()
handler := NewHandler(fs)
aferoHelper := afero.Afero{Fs: fs}
require.NoError(aferoHelper.WriteFile("a", []byte{0xa}, 0o644))
require.NoError(aferoHelper.WriteFile("b", []byte{0xb}, 0o644))
require.NoError(aferoHelper.WriteFile("c", []byte{0xc}, 0o644))
assert.NoError(handler.Remove("a"))
assert.NoError(handler.Remove("b"))
assert.NoError(handler.Remove("c"))
_, err := handler.fs.Stat("a")
assert.ErrorIs(err, afero.ErrFileNotFound)
_, err = handler.fs.Stat("b")
assert.ErrorIs(err, afero.ErrFileNotFound)
_, err = handler.fs.Stat("c")
assert.ErrorIs(err, afero.ErrFileNotFound)
assert.Error(handler.Remove("d"))
}
cli: Terraform migrations on upgrade (#1685) * add terraform planning * overwrite terraform files in upgrade workspace * Revert "overwrite terraform files in upgrade workspace" This reverts commit 8bdacfb8bef23ef2cdbdb06bad0855b3bbc42df0. * prepare terraform workspace * test upgrade integration * print upgrade abort * rename plan file * write output to file * add show plan test * add upgrade tf workdir * fix workspace preparing * squash to 1 command * test * bazel build * plan test * register flag manually * bazel tidy * fix linter * remove MAA variable * fix workdir * accept tf variables * variable fetching * fix resource indices * accept Terraform targets * refactor upgrade command * Terraform migration apply unit test * pass down image fetcher to test * use new flags in e2e test * move file name to constant * update buildfiles * fix version constant * conditionally create MAA * move interface down * upgrade dir * update buildfiles * fix interface * fix createMAA check * fix imports * update buildfiles * wip: workspace backup * copy utils * backup upgrade workspace * remove debug print * replace old state after upgrade * check if flag exists * prepare test workspace * remove prefix Co-authored-by: Otto Bittner <cobittner@posteo.net> * respect file permissions * refactor tf upgrader * check workspace before upgrades * remove temp upgrade dir after completion * clean up workspace after abortion * fix upgrade apply test * fix linter --------- Co-authored-by: Otto Bittner <cobittner@posteo.net>
2023-05-22 07:31:20 -04:00
func TestCopyFile(t *testing.T) {
perms := fs.FileMode(0o644)
setupFs := func(existingFiles ...string) afero.Fs {
fs := afero.NewMemMapFs()
aferoHelper := afero.Afero{Fs: fs}
for _, file := range existingFiles {
require.NoError(t, aferoHelper.WriteFile(file, []byte{}, perms))
}
return fs
}
testCases := map[string]struct {
fs afero.Fs
copyFiles [][]string
checkFiles []string
opts []Option
wantErr bool
}{
"successful copy": {
fs: setupFs("a"),
copyFiles: [][]string{{"a", "b"}},
checkFiles: []string{"b"},
},
"copy to existing file overwrite": {
fs: setupFs("a", "b"),
copyFiles: [][]string{{"a", "b"}},
checkFiles: []string{"b"},
opts: []Option{OptOverwrite},
},
"copy to existing file no overwrite": {
fs: setupFs("a", "b"),
copyFiles: [][]string{{"a", "b"}},
checkFiles: []string{"b"},
wantErr: true,
},
"file doesn't exist": {
fs: setupFs("a"),
copyFiles: [][]string{{"b", "c"}},
checkFiles: []string{"a"},
wantErr: true,
},
}
for name, tc := range testCases {
t.Run(name, func(t *testing.T) {
assert := assert.New(t)
require := require.New(t)
handler := NewHandler(tc.fs)
for _, files := range tc.copyFiles {
err := handler.CopyFile(files[0], files[1], tc.opts...)
if tc.wantErr {
assert.Error(err)
} else {
assert.NoError(err)
}
}
for _, file := range tc.checkFiles {
info, err := handler.fs.Stat(file)
assert.Equal(perms, info.Mode())
require.NoError(err)
}
})
}
}
func TestCopyDir(t *testing.T) {
setupHandler := func(existingFiles ...string) Handler {
fs := afero.NewMemMapFs()
handler := NewHandler(fs)
for _, file := range existingFiles {
err := handler.Write(file, []byte("some content"), OptMkdirAll)
require.NoError(t, err)
}
return handler
}
testCases := map[string]struct {
handler Handler
copyFiles [][]string
checkFiles []string
opts []Option
}{
"successful copy": {
handler: setupHandler(filepath.Join("someDir", "someFile"), filepath.Join("someDir", "someOtherDir", "someOtherFile")),
copyFiles: [][]string{{"someDir", "copiedDir"}},
checkFiles: []string{filepath.Join("copiedDir", "someFile"), filepath.Join("copiedDir", "someOtherDir", "someOtherFile")},
},
"copy file": {
handler: setupHandler("someFile"),
copyFiles: [][]string{{"someFile", "copiedFile"}},
checkFiles: []string{"copiedFile"},
},
"copy to existing dir overwrite": {
handler: setupHandler(filepath.Join("someDir", "someFile"), filepath.Join("someDir", "someOtherDir", "someOtherFile"), filepath.Join("copiedDir", "someExistingFile")),
copyFiles: [][]string{{"someDir", "copiedDir"}},
checkFiles: []string{filepath.Join("copiedDir", "someFile"), filepath.Join("copiedDir", "someOtherDir", "someOtherFile"), filepath.Join("copiedDir", "someExistingFile")},
opts: []Option{OptOverwrite},
},
}
for name, tc := range testCases {
t.Run(name, func(t *testing.T) {
require := require.New(t)
for _, files := range tc.copyFiles {
err := tc.handler.CopyDir(files[0], files[1], tc.opts...)
require.NoError(err)
}
for _, file := range tc.checkFiles {
_, err := tc.handler.fs.Stat(file)
require.NoError(err)
}
})
}
}
cli: use state file on init and upgrade (#2395) * [wip] use state file in CLI Signed-off-by: Moritz Sanft <58110325+msanft@users.noreply.github.com> tidy Signed-off-by: Moritz Sanft <58110325+msanft@users.noreply.github.com> * use state file in CLI Signed-off-by: Moritz Sanft <58110325+msanft@users.noreply.github.com> take clusterConfig from IDFile for compat Signed-off-by: Moritz Sanft <58110325+msanft@users.noreply.github.com> various fixes Signed-off-by: Moritz Sanft <58110325+msanft@users.noreply.github.com> wip Signed-off-by: Moritz Sanft <58110325+msanft@users.noreply.github.com> * add GCP-specific values in Helm loader test Signed-off-by: Moritz Sanft <58110325+msanft@users.noreply.github.com> * remove unnecessary pointer Signed-off-by: Moritz Sanft <58110325+msanft@users.noreply.github.com> * write ClusterValues in one step Signed-off-by: Moritz Sanft <58110325+msanft@users.noreply.github.com> * move stub to test file Signed-off-by: Moritz Sanft <58110325+msanft@users.noreply.github.com> * remove mention of id-file Signed-off-by: Moritz Sanft <58110325+msanft@users.noreply.github.com> * move output to `migrateTerraform` Signed-off-by: Moritz Sanft <58110325+msanft@users.noreply.github.com> * unconditional assignments converting from idFile Signed-off-by: Moritz Sanft <58110325+msanft@users.noreply.github.com> * move require block in go modules file Signed-off-by: Moritz Sanft <58110325+msanft@users.noreply.github.com> * fall back to id file on upgrade Signed-off-by: Moritz Sanft <58110325+msanft@users.noreply.github.com> * tidy Signed-off-by: Moritz Sanft <58110325+msanft@users.noreply.github.com> * fix linter check Signed-off-by: Moritz Sanft <58110325+msanft@users.noreply.github.com> * add notice to remove Terraform state check on manual migration Signed-off-by: Moritz Sanft <58110325+msanft@users.noreply.github.com> * add `name` field Signed-off-by: Moritz Sanft <58110325+msanft@users.noreply.github.com> fix name tests Signed-off-by: Moritz Sanft <58110325+msanft@users.noreply.github.com> * return early if no Terraform diff Signed-off-by: Moritz Sanft <58110325+msanft@users.noreply.github.com> * tidy Signed-off-by: Moritz Sanft <58110325+msanft@users.noreply.github.com> * return infrastructure state even if no diff exists Signed-off-by: Moritz Sanft <58110325+msanft@users.noreply.github.com> * add TODO to remove comment Signed-off-by: Moritz Sanft <58110325+msanft@users.noreply.github.com> * use state-file in miniconstellation Signed-off-by: Moritz Sanft <58110325+msanft@users.noreply.github.com> * cli: remove id-file (#2402) * remove id-file from `constellation create` Signed-off-by: Moritz Sanft <58110325+msanft@users.noreply.github.com> * add file renaming to handler * rename id-file after upgrade * use idFile on `constellation init` Signed-off-by: Moritz Sanft <58110325+msanft@users.noreply.github.com> * remove id-file from `constellation verify` Signed-off-by: Moritz Sanft <58110325+msanft@users.noreply.github.com> * linter fixes Signed-off-by: Moritz Sanft <58110325+msanft@users.noreply.github.com> * remove id-file from `constellation mini` * remove id-file from `constellation recover` * linter fixes * remove id-file from `constellation terminate` * fix initSecret type * fix recover argument precedence * fix terminate test * generate * add TODO to remove id-file removal * Update cli/internal/cmd/init.go Co-authored-by: Adrian Stobbe <stobbe.adrian@gmail.com> * fix verify arg parse logic Signed-off-by: Moritz Sanft <58110325+msanft@users.noreply.github.com> * add version test Signed-off-by: Moritz Sanft <58110325+msanft@users.noreply.github.com> * remove id-file from docs * add file not found log * use state-file in miniconstellation Signed-off-by: Moritz Sanft <58110325+msanft@users.noreply.github.com> * remove id-file from `constellation iam destroy` Signed-off-by: Moritz Sanft <58110325+msanft@users.noreply.github.com> * remove id-file from `cdbg deploy` Signed-off-by: Moritz Sanft <58110325+msanft@users.noreply.github.com> --------- Signed-off-by: Moritz Sanft <58110325+msanft@users.noreply.github.com> Co-authored-by: Adrian Stobbe <stobbe.adrian@gmail.com> * use state-file in CI Signed-off-by: Moritz Sanft <58110325+msanft@users.noreply.github.com> * update orchestration docs --------- Signed-off-by: Moritz Sanft <58110325+msanft@users.noreply.github.com> Co-authored-by: Adrian Stobbe <stobbe.adrian@gmail.com>
2023-10-09 07:04:29 -04:00
func TestRename(t *testing.T) {
setupHandler := func(existingFiles ...string) Handler {
fs := afero.NewMemMapFs()
handler := NewHandler(fs)
for _, file := range existingFiles {
err := handler.Write(file, []byte("some content"), OptMkdirAll)
require.NoError(t, err)
}
return handler
}
testCases := map[string]struct {
handler Handler
renames map[string]string
checkFiles []string
wantErr bool
}{
"successful rename": {
handler: setupHandler("someFile"),
renames: map[string]string{"someFile": "someOtherFile"},
checkFiles: []string{"someOtherFile"},
},
"rename to existing file, overwrite": {
handler: setupHandler("someFile", "someOtherFile"),
renames: map[string]string{"someFile": "someOtherFile"},
checkFiles: []string{"someOtherFile"},
},
"file does not exist": {
handler: setupHandler(),
renames: map[string]string{"someFile": "someOtherFile"},
wantErr: true,
},
}
for name, tc := range testCases {
t.Run(name, func(t *testing.T) {
require := require.New(t)
for old, new := range tc.renames {
err := tc.handler.RenameFile(old, new)
if tc.wantErr {
require.Error(err)
} else {
require.NoError(err)
}
}
for _, file := range tc.checkFiles {
_, err := tc.handler.fs.Stat(file)
require.NoError(err)
}
})
}
}
func TestIsEmpty(t *testing.T) {
testCases := map[string]struct {
setupFs func(fs *afero.Afero, dirName string) error
wantIsEmpty bool
wantErr bool
}{
"empty directory": {
setupFs: func(fs *afero.Afero, dirName string) error { return fs.Mkdir(dirName, 0o755) },
wantIsEmpty: true,
},
"directory not empty": {
setupFs: func(fs *afero.Afero, dirName string) error {
return fs.WriteFile(filepath.Join(dirName, "file"), []byte("some content"), 0o755)
},
},
"directory not existent": {
wantErr: true,
},
}
for name, tc := range testCases {
t.Run(name, func(t *testing.T) {
assert := assert.New(t)
require := require.New(t)
dirName := "test"
handler := NewHandler(afero.NewMemMapFs())
if tc.setupFs != nil {
require.NoError(tc.setupFs(handler.fs, dirName))
}
isEmpty, err := handler.IsEmpty(dirName)
if tc.wantErr {
assert.Error(err)
} else {
assert.NoError(err)
assert.Equal(tc.wantIsEmpty, isEmpty)
}
})
}
}