diff --git a/.github/workflows/go.yaml b/.github/workflows/go.yaml index eebc3b2..4c35f8e 100644 --- a/.github/workflows/go.yaml +++ b/.github/workflows/go.yaml @@ -40,7 +40,7 @@ jobs: cache-dependency-path: src/go/go.sum go-version-file: src/go/go.mod - - run: go build -v ./... + - run: make all go-test: defaults: diff --git a/src/go/coremetarepo/meta_data_admin.go b/src/go/coremetarepo/meta_data_admin.go index 39da84b..44a4ae3 100644 --- a/src/go/coremetarepo/meta_data_admin.go +++ b/src/go/coremetarepo/meta_data_admin.go @@ -4,4 +4,7 @@ package coremetarepo type MetaDataAdmin interface { // Create a meta repository at the specified path on the local filesystem. Create(metaRepoPath string) error + + // Returns true if the specified path exists and is already a meta repository. + IsMetaRepo(metaRepoPath string) (bool, error) } diff --git a/src/go/coremetarepomock/meta_data_admin_mock.go b/src/go/coremetarepomock/meta_data_admin_mock.go index 3e34ff1..f5e4f1e 100644 --- a/src/go/coremetarepomock/meta_data_admin_mock.go +++ b/src/go/coremetarepomock/meta_data_admin_mock.go @@ -7,13 +7,18 @@ import ( // Construct a test double for MetaDataAdmin. func NewMetaDataAdmin() *MetaDataAdmin { - return &MetaDataAdmin{} + return &MetaDataAdmin{ + isMetaRepoError: make(map[string]error), + isMetaRepoReturns: make(map[string]bool), + } } // Mock implementation for testing with MetaDataAdmin. type MetaDataAdmin struct { - createCalls []string - createError error + createCalls []string + createError error + isMetaRepoError map[string]error + isMetaRepoReturns map[string]bool } func (admin *MetaDataAdmin) Create(metaRepoPath string) error { @@ -21,13 +26,20 @@ func (admin *MetaDataAdmin) Create(metaRepoPath string) error { return admin.createError } -// Assert that a meta repo was created at the specified path. func (admin *MetaDataAdmin) CreateExpected(expectedPath string) { ginkgo.GinkgoHelper() Expect(admin.createCalls).To(ContainElement(expectedPath)) } -// Stub #Create to fail with the given error. func (admin *MetaDataAdmin) CreateFails(err error) { admin.createError = err } + +func (admin *MetaDataAdmin) IsMetaRepo(path string) (bool, error) { + return admin.isMetaRepoReturns[path], admin.isMetaRepoError[path] +} + +func (admin *MetaDataAdmin) IsMetaRepoReturns(path string, value bool, err error) { + admin.isMetaRepoReturns[path] = value + admin.isMetaRepoError[path] = err +} diff --git a/src/go/cukesupport/meta_repo_hook.go b/src/go/cukesupport/meta_repo_hook.go index 065dd1b..d60207e 100644 --- a/src/go/cukesupport/meta_repo_hook.go +++ b/src/go/cukesupport/meta_repo_hook.go @@ -20,7 +20,7 @@ func addMetaRepoFixtureAfterLocalDir(ctx *godog.ScenarioContext) { } func afterMetaRepo(ctx context.Context, sc *godog.Scenario, err error) (context.Context, error) { - //Always clear state; it could have been initialized by a hook _or_ an explicit step + // Always clear state; it could have been initialized by a hook _or_ an explicit step forgetThatMetaRepo() return ctx, err } diff --git a/src/go/svcfs/json_meta_repo_admin.go b/src/go/svcfs/json_meta_repo_admin.go index 1fe6f12..5ecbf11 100644 --- a/src/go/svcfs/json_meta_repo_admin.go +++ b/src/go/svcfs/json_meta_repo_admin.go @@ -27,7 +27,8 @@ func (admin *JsonMetaRepoAdmin) Create(repositoryDir string) error { } else if statErr != nil { return fmt.Errorf("failed to check for existing meta repo %s; %w", repositoryDir, statErr) } else { - return fmt.Errorf("path already exists: %s", marmotDataDir) + // Ignore an existing meta repo, for now + return nil } } @@ -41,3 +42,24 @@ func initDirectory(metaDataFile string, rootObject *rootObjectData) error { return nil } } + +func (admin *JsonMetaRepoAdmin) IsMetaRepo(path string) (bool, error) { + pathStat, pathErr := os.Stat(path) + if errors.Is(pathErr, fs.ErrNotExist) { + return false, nil + } else if pathErr != nil { + return false, fmt.Errorf("%s: failed to stat meta repo path; %w", path, pathErr) + } else if pathStat.Mode().IsRegular() { + return false, nil + } + + marmotDir := metaDataDir(path) + _, marmotDirErr := os.Stat(marmotDir) + if errors.Is(marmotDirErr, fs.ErrNotExist) { + return false, nil + } else if marmotDirErr != nil { + return false, fmt.Errorf("%s: failed to stat Marmot directory; %w", marmotDir, marmotDirErr) + } else { + return true, nil + } +} diff --git a/src/go/svcfs/json_meta_repo_admin_test.go b/src/go/svcfs/json_meta_repo_admin_test.go index 3b9a1cc..fe0acf9 100644 --- a/src/go/svcfs/json_meta_repo_admin_test.go +++ b/src/go/svcfs/json_meta_repo_admin_test.go @@ -1,7 +1,6 @@ package svcfs_test import ( - "fmt" "io/fs" "os" "path/filepath" @@ -13,59 +12,90 @@ import ( . "github.com/onsi/gomega" ) +var testFsRoot string + var _ = Describe("JsonMetaRepoAdmin", func() { - var ( - subject *svcfs.JsonMetaRepoAdmin - metaRepoPath string - testFsRoot string - ) + var subject *svcfs.JsonMetaRepoAdmin BeforeEach(func() { testFsRoot = expect.NoError(os.MkdirTemp("", "JsonMetaDataRepo-")) - metaRepoPath = filepath.Join(testFsRoot, "meta") DeferCleanup(os.RemoveAll, testFsRoot) }) Describe("#Create", func() { - It("is cool with an existing path in which marmot has not been initialized", func() { - Expect(os.MkdirAll(metaRepoPath, fs.ModePerm)).To(Succeed()) + It("creates files in the directory, given a valid, writable path", func() { + subject = jsonMetaRepoAdmin(nil) + subject.Create(testFsRoot) + metaDataDir := filepath.Join(testFsRoot, ".marmot") + Expect(os.Stat(metaDataDir)).NotTo(BeNil()) + + metaDataFile := filepath.Join(metaDataDir, "meta-repo.json") + Expect(os.Stat(metaDataFile)).NotTo(BeNil()) + }) + + It("returns no error, upon success", func() { subject = jsonMetaRepoAdmin(nil) - Expect(subject.Create(metaRepoPath)).To(Succeed()) + Expect(subject.Create(testFsRoot)).To(Succeed()) }) - It("returns an error, given a path containing marmot data", func() { - marmotDataDir := filepath.Join(metaRepoPath, ".marmot") + It("accepts an existing directory that is not a Marmot repo", func() { + Expect(os.MkdirAll(testFsRoot, fs.ModePerm)).To(Succeed()) + subject = jsonMetaRepoAdmin(nil) + Expect(subject.Create(testFsRoot)).To(Succeed()) + }) + + It("ignores an existing directory already containing Marmot data", func() { + // What about the files inside of .marmot/? Should those be re-created or left alone? + marmotDataDir := filepath.Join(testFsRoot, ".marmot") Expect(os.MkdirAll(marmotDataDir, fs.ModePerm)).To(Succeed()) subject = jsonMetaRepoAdmin(nil) - Expect(subject.Create(metaRepoPath)).To( - MatchError(fmt.Sprintf("path already exists: %s", marmotDataDir))) + Expect(subject.Create(testFsRoot)).To(Succeed()) }) - It("returns an error when unable to check if the path exists", func() { + It("returns an error, given an invalid path", func() { subject = jsonMetaRepoAdmin(nil) - invalidPathErr := subject.Create("\000x") - Expect(invalidPathErr).NotTo(BeNil()) + Expect(subject.Create("\000x")).To( + MatchError(MatchRegexp("failed to check for existing meta repo"))) }) - It("returns an error when creating files fails", func() { + It("returns an error, given a path in which files can not be created", func() { Expect(os.Chmod(testFsRoot, 0o555)).To(Succeed()) - subject = jsonMetaRepoAdmin(nil) - Expect(subject.Create(metaRepoPath)).To( - MatchError(ContainSubstring(fmt.Sprintf("failed to make directory %s", metaRepoPath)))) + Expect(subject.Create(testFsRoot)).To( + MatchError(MatchRegexp("failed to make directory"))) }) + }) - It("creates files in the meta repository and returns nil, otherwise", func() { + Describe("#IsMetaRepo", func() { + BeforeEach(func() { subject = jsonMetaRepoAdmin(nil) - Expect(subject.Create(metaRepoPath)).To(Succeed()) + }) - metaDataDir := filepath.Join(metaRepoPath, ".marmot") - Expect(os.Stat(metaDataDir)).NotTo(BeNil()) + It("returns false, given a non-existent path", func() { + Expect(subject.IsMetaRepo(nonExistentPath())).To(Equal(false)) + }) - metaDataFile := filepath.Join(metaDataDir, "meta-repo.json") - Expect(os.Stat(metaDataFile)).NotTo(BeNil()) + It("returns false, given an existing path that is not a directory", func() { + existingFile := expect.NoError(someFile()) + Expect(subject.IsMetaRepo(existingFile)).To(Equal(false)) + }) + + It("returns false, given a directory not containing a Marmot metadata", func() { + Expect(subject.IsMetaRepo(existingPath())).To(Equal(false)) + }) + + It("returns true, given a directory containing Marmot metadata", func() { + marmotDataDir := filepath.Join(testFsRoot, ".marmot") + Expect(os.MkdirAll(marmotDataDir, fs.ModePerm)).To(Succeed()) + + Expect(subject.IsMetaRepo(testFsRoot)).To(Equal(true)) + }) + + It("returns an error, when checking that path fails", func() { + _, err := subject.IsMetaRepo("\000x") + Expect(err).To(MatchError(ContainSubstring("failed to stat meta repo path"))) }) }) }) @@ -89,3 +119,19 @@ func (args jsonMetaRepoAdminArgs) Version() string { return args.version } } + +/* Filesystem */ + +func existingPath() string { return testFsRoot } + +func someFile() (string, error) { + path := filepath.Join(testFsRoot, "existing-file") + if aFile, createErr := os.Create(path); createErr != nil { + return "", createErr + } else { + defer aFile.Close() + return path, nil + } +} + +func nonExistentPath() string { return filepath.Join(testFsRoot, "not-created-yet") } diff --git a/src/go/usemetarepo/init_command.go b/src/go/usemetarepo/init_command.go index fab5ba3..f729630 100644 --- a/src/go/usemetarepo/init_command.go +++ b/src/go/usemetarepo/init_command.go @@ -1,6 +1,10 @@ package usemetarepo -import core "github.com/kkrull/marmot/coremetarepo" +import ( + "fmt" + + core "github.com/kkrull/marmot/coremetarepo" +) // Initializes a new meta repo where none existed before. type InitCommand struct { @@ -8,5 +12,13 @@ type InitCommand struct { } func (cmd InitCommand) Run(metaRepoPath string) error { - return cmd.MetaDataAdmin.Create(metaRepoPath) + if isMetaRepo, isMetaRepoErr := cmd.MetaDataAdmin.IsMetaRepo(metaRepoPath); isMetaRepoErr != nil { + return fmt.Errorf("%s: unable to check path; %w", metaRepoPath, isMetaRepoErr) + } else if isMetaRepo { + return fmt.Errorf("%s: already a meta repo", metaRepoPath) + } else if createErr := cmd.MetaDataAdmin.Create(metaRepoPath); createErr != nil { + return fmt.Errorf("failed to initialize meta repo; %w", createErr) + } else { + return nil + } } diff --git a/src/go/usemetarepo/init_command_test.go b/src/go/usemetarepo/init_command_test.go index 8fb49f4..287bdc0 100644 --- a/src/go/usemetarepo/init_command_test.go +++ b/src/go/usemetarepo/init_command_test.go @@ -2,6 +2,8 @@ package usemetarepo_test import ( "errors" + "os" + "path/filepath" mock "github.com/kkrull/marmot/coremetarepomock" expect "github.com/kkrull/marmot/testsupportexpect" @@ -12,6 +14,12 @@ import ( . "github.com/onsi/gomega" ) +var testDir string + +func existingPath() string { return testDir } +func nonExistentPath() string { return filepath.Join(testDir, "not-created-yet") } +func validPath() string { return filepath.Join(testDir, "meta-default") } + var _ = Describe("InitCommand", func() { var ( subject *usemetarepo.InitCommand @@ -19,24 +27,48 @@ var _ = Describe("InitCommand", func() { ) BeforeEach(func() { + testDir = expect.NoError(os.MkdirTemp("", "InitCommand-")) + DeferCleanup(os.RemoveAll, testDir) + metaDataAdmin = mock.NewMetaDataAdmin() factory := use.NewCommandFactory().WithMetaDataAdmin(metaDataAdmin) subject = expect.NoError(factory.NewInitMetaRepo()) }) Describe("#Run", func() { - It("initializes the given meta data source", func() { - _ = subject.Run("/tmp") - metaDataAdmin.CreateExpected("/tmp") + It("creates a meta repo in that path", func() { + givenPath := validPath() + subject.Run(givenPath) + metaDataAdmin.CreateExpected(givenPath) + }) + + It("returns no error, upon success", func() { + Expect(subject.Run(validPath())).To(Succeed()) + }) + + It("accepts paths that do and do not exist, provided a meta repo is not there", func() { + Expect(subject.Run(existingPath())).To(Succeed()) + Expect(subject.Run(nonExistentPath())).To(Succeed()) + }) + + It("returns an error when unable to check the path", func() { + path := filepath.Join(testDir, "stealth") + metaDataAdmin.IsMetaRepoReturns(path, false, errors.New("bang!")) + Expect(subject.Run(path)).To( + MatchError(ContainSubstring("stealth: unable to check path; bang!"))) }) - It("returns nil, when everything succeeds", func() { - Expect(subject.Run("/tmp")).To(BeNil()) + It("returns an error when the path is already a meta repo", func() { + existingMetaRepo := filepath.Join(testDir, "meta-already") + metaDataAdmin.IsMetaRepoReturns(existingMetaRepo, true, nil) + Expect(subject.Run(existingMetaRepo)).To( + MatchError(MatchRegexp("meta-already: already a meta repo$"))) }) - It("returns an error when failing to initialize the meta data source", func() { + It("returns an error when creating a meta repo fails", func() { metaDataAdmin.CreateFails(errors.New("bang!")) - Expect(subject.Run("/tmp")).To(MatchError("bang!")) + Expect(subject.Run(validPath())).To( + MatchError(MatchRegexp("^failed to initialize meta repo.*bang!$"))) }) }) })