diff --git a/.gitignore b/.gitignore index cc287f2..64fcbab 100644 --- a/.gitignore +++ b/.gitignore @@ -16,3 +16,6 @@ mock_* # Output of the go coverage tool, specifically when used with LiteIDE *.out + +# Integration test output +test/data/generated.yml diff --git a/README.md b/README.md index 5dee880..2ab9e92 100644 --- a/README.md +++ b/README.md @@ -15,6 +15,22 @@ combine the library and template to compose the final document. - compose: interpolate for all snippets defined in a set of scenarios # subcommands +## import +``` +import [--recursive] --path --out : + create a library from a directory of opsfiles. + -o string + Path to save generated library file + -out string + Path to save generated library file + -p string + Directory or opsfile to import + -path string + Directory or opsfile to import + -r Import opsfiles from subdirectories + -recursive + Import opsfiles from subdirectories +``` ## list ``` ./manifer list [--all] (--library ...): diff --git a/cmd/commands/import.go b/cmd/commands/import.go new file mode 100644 index 0000000..35fe178 --- /dev/null +++ b/cmd/commands/import.go @@ -0,0 +1,87 @@ +package commands + +import ( + "context" + "flag" + "io" + "log" + + "github.com/google/subcommands" + + "github.com/cjnosal/manifer/lib" + "github.com/cjnosal/manifer/pkg/file" + "github.com/cjnosal/manifer/pkg/library" + "github.com/cjnosal/manifer/pkg/yaml" +) + +type importCmd struct { + out string + path string + recursive bool + + logger *log.Logger + writer io.Writer + manifer lib.Manifer +} + +func NewImportCommand(l io.Writer, w io.Writer, m lib.Manifer) subcommands.Command { + return &importCmd{ + logger: log.New(l, "", 0), + writer: w, + manifer: m, + } +} + +func (*importCmd) Name() string { return "import" } +func (*importCmd) Synopsis() string { return "create a library from a directory of opsfiles." } +func (*importCmd) Usage() string { + return `import [--recursive] --path --out : + create a library from a directory of opsfiles. +` +} + +func (p *importCmd) SetFlags(f *flag.FlagSet) { + f.StringVar(&p.out, "out", "", "Path to save generated library file") + f.StringVar(&p.out, "o", "", "Path to save generated library file") + f.StringVar(&p.path, "path", "", "Directory or opsfile to import") + f.StringVar(&p.path, "p", "", "Directory or opsfile to import") + f.BoolVar(&p.recursive, "recursive", false, "Import opsfiles from subdirectories") + f.BoolVar(&p.recursive, "r", false, "Import opsfiles from subdirectories") +} + +func (p *importCmd) Execute(_ context.Context, f *flag.FlagSet, _ ...interface{}) subcommands.ExitStatus { + + if len(p.path) == 0 { + p.logger.Printf("Import path not specified") + p.logger.Printf(p.Usage()) + return subcommands.ExitFailure + } + if len(p.out) == 0 { + p.logger.Printf("Output path not specified") + p.logger.Printf(p.Usage()) + return subcommands.ExitFailure + } + + lib, err := p.manifer.Import(library.OpsFile, p.path, p.recursive, p.out) + + if err != nil { + p.logger.Printf("%v\n while generating library", err) + return subcommands.ExitFailure + } + + yaml := &yaml.Yaml{} + outBytes, err := yaml.Marshal(lib) + if err != nil { + p.logger.Printf("%v\n while marshaling generated library", err) + return subcommands.ExitFailure + } + + file := &file.FileIO{} + err = file.Write(p.out, outBytes, 0644) + if err != nil { + p.logger.Printf("%v\n while writing generated library", err) + return subcommands.ExitFailure + } + + return subcommands.ExitSuccess +} diff --git a/cmd/manifer/manifer.go b/cmd/manifer/manifer.go index 7f65f43..840476d 100644 --- a/cmd/manifer/manifer.go +++ b/cmd/manifer/manifer.go @@ -26,6 +26,7 @@ func main() { subcommands.Register(commands.NewListCommand(logger, writer, maniferLib), "") subcommands.Register(commands.NewSearchCommand(logger, writer, maniferLib), "") subcommands.Register(commands.NewInspectCommand(logger, writer, maniferLib), "") + subcommands.Register(commands.NewImportCommand(logger, writer, maniferLib), "") // run flag.Parse() diff --git a/cmd/manifer/manifer_test.go b/cmd/manifer/manifer_test.go index fde1e3a..63791b1 100644 --- a/cmd/manifer/manifer_test.go +++ b/cmd/manifer/manifer_test.go @@ -381,4 +381,121 @@ dependencies: }) }) + + t.Run("TestImport file", func(t *testing.T) { + cmd := exec.Command( + "../../manifer", + "import", + "-p", + "../../test/data/opsfile.yml", + "-o", + "../../test/data/generated.yml", + ) + + err := cmd.Run() + if err != nil { + t.Errorf("Unexpected error: %v", err) + } + + cat := exec.Command( + "cat", + "../../test/data/generated.yml", + ) + outWriter := &test.StringWriter{} + cat.Stdout = outWriter + + err = cat.Run() + if err != nil { + t.Errorf("Unexpected error: %v", err) + } + + expectedOut := `libraries: [] +type: opsfile +scenarios: + - name: opsfile + description: imported from opsfile.yml + global_args: [] + args: [] + snippets: + - path: opsfile.yml + args: [] + scenarios: [] +` + + if !cmp.Equal(outWriter.String(), expectedOut) { + t.Errorf("Expected Stdout:\n'''%v'''\nActual:\n'''%v'''\nDiff:\n'''%v'''\n", + expectedOut, outWriter.String(), cmp.Diff(expectedOut, outWriter.String())) + } + }) + + t.Run("TestImport directory", func(t *testing.T) { + cmd := exec.Command( + "../../manifer", + "import", + "-r", + "-p", + "../../test/data", + "-o", + "../../test/data/generated.yml", + ) + + err := cmd.Run() + if err != nil { + t.Errorf("Unexpected error: %v", err) + } + + cat := exec.Command( + "cat", + "../../test/data/generated.yml", + ) + outWriter := &test.StringWriter{} + cat.Stdout = outWriter + + err = cat.Run() + if err != nil { + t.Errorf("Unexpected error: %v", err) + } + + expectedOut := `libraries: [] +type: opsfile +scenarios: + - name: empty_opsfile + description: imported from empty_opsfile.yml + global_args: [] + args: [] + snippets: + - path: empty_opsfile.yml + args: [] + scenarios: [] + - name: opsfile + description: imported from opsfile.yml + global_args: [] + args: [] + snippets: + - path: opsfile.yml + args: [] + scenarios: [] + - name: opsfile_with_vars + description: imported from opsfile_with_vars.yml + global_args: [] + args: [] + snippets: + - path: opsfile_with_vars.yml + args: [] + scenarios: [] + - name: placeholder_opsfile + description: imported from placeholder_opsfile.yml + global_args: [] + args: [] + snippets: + - path: placeholder_opsfile.yml + args: [] + scenarios: [] +` + + if !cmp.Equal(outWriter.String(), expectedOut) { + t.Errorf("Expected Stdout:\n'''%v'''\nActual:\n'''%v'''\nDiff:\n'''%v'''\n", + expectedOut, outWriter.String(), cmp.Diff(expectedOut, outWriter.String())) + } + }) } diff --git a/lib/maniferlib.go b/lib/maniferlib.go index 7446d39..54ca7ef 100644 --- a/lib/maniferlib.go +++ b/lib/maniferlib.go @@ -7,6 +7,7 @@ import ( "github.com/cjnosal/manifer/pkg/composer" "github.com/cjnosal/manifer/pkg/diff" "github.com/cjnosal/manifer/pkg/file" + "github.com/cjnosal/manifer/pkg/importer" "github.com/cjnosal/manifer/pkg/interpolator" "github.com/cjnosal/manifer/pkg/interpolator/opsfile" "github.com/cjnosal/manifer/pkg/library" @@ -18,12 +19,15 @@ import ( // logger used for Composer's showDiff/showPlan func NewManifer(logger io.Writer) Manifer { + fileIO := &file.FileIO{} + opsFileInterpolator := opsfile.NewOpsFileInterpolator(&yaml.Yaml{}, fileIO) return &libImpl{ composer: newComposer(logger), lister: newLister(), loader: newLoader(), - file: &file.FileIO{}, - opInt: opsfile.NewOpsFileInterpolator(&yaml.Yaml{}), + file: fileIO, + opInt: opsFileInterpolator, + importer: importer.NewImporter(fileIO, opsFileInterpolator), } } @@ -49,6 +53,8 @@ type Manifer interface { GetScenarioTree(libraryPaths []string, name string) (*library.ScenarioNode, error) GetScenarioNode(passthroughArgs []string) (*library.ScenarioNode, error) + + Import(libType library.Type, path string, recursive bool, outPath string) (*library.Library, error) } type libImpl struct { @@ -57,6 +63,7 @@ type libImpl struct { loader *library.Loader file *file.FileIO opInt interpolator.Interpolator + importer importer.Importer } func (l *libImpl) Compose( @@ -108,6 +115,10 @@ func (l *libImpl) GetScenarioNode(passthroughArgs []string) (*library.ScenarioNo return l.opInt.ParsePassthroughFlags(passthroughArgs) } +func (l *libImpl) Import(libType library.Type, path string, recursive bool, outPath string) (*library.Library, error) { + return l.importer.Import(libType, path, recursive, outPath) +} + func (l *libImpl) makePathsRelative(node *library.ScenarioNode) error { for i, snippet := range node.Snippets { rel, err := l.file.ResolveRelativeFromWD(snippet.Path) @@ -166,7 +177,7 @@ func newComposer(logger io.Writer) composer.Composer { File: file, Yaml: yaml, } - opsFileInterpolator := opsfile.NewOpsFileInterpolator(yaml) + opsFileInterpolator := opsfile.NewOpsFileInterpolator(yaml, file) resolver := &composer.Resolver{ Loader: loader, SnippetResolver: opsFileInterpolator, diff --git a/pkg/file/file.go b/pkg/file/file.go index 51fe5d4..ad42285 100644 --- a/pkg/file/file.go +++ b/pkg/file/file.go @@ -16,6 +16,8 @@ type FileAccess interface { ResolveRelativeFrom(targetFile string, sourceFile string) (string, error) ResolveRelativeFromWD(targetFile string) (string, error) GetWorkingDirectory() (string, error) + IsDir(path string) (bool, error) + Walk(path string, callback func(path string, info os.FileInfo, err error) error) error } type FileIO struct{} @@ -57,11 +59,11 @@ func (f *FileIO) ResolveRelativeTo(targetFile string, sourceFile string) (string return targetFile, nil } else { dir := sourceFile - dirInfo, err := os.Stat(dir) + isDir, err := f.IsDir(dir) if err != nil { return "", err } - if !dirInfo.IsDir() { + if !isDir { dir = filepath.Dir(sourceFile) } return filepath.Clean(filepath.Join(dir, targetFile)), nil @@ -69,18 +71,25 @@ func (f *FileIO) ResolveRelativeTo(targetFile string, sourceFile string) (string } func (f *FileIO) ResolveRelativeFrom(targetFile string, sourceFile string) (string, error) { - if !filepath.IsAbs(targetFile) { - return targetFile, nil + dir, err := filepath.Abs(sourceFile) + if err != nil { + return "", err } - dir := sourceFile - dirInfo, err := os.Stat(dir) + isDir, err := f.IsDir(dir) if err != nil { return "", err } - if !dirInfo.IsDir() { - dir = filepath.Dir(sourceFile) + if !isDir { + dir, err = filepath.Abs(filepath.Dir(sourceFile)) + if err != nil { + return "", err + } } - return filepath.Rel(dir, targetFile) + target, err := filepath.Abs(targetFile) + if err != nil { + return "", err + } + return filepath.Rel(dir, target) } func (f *FileIO) ResolveRelativeFromWD(targetFile string) (string, error) { @@ -97,3 +106,15 @@ func (f *FileIO) ResolveRelativeFromWD(targetFile string) (string, error) { func (f *FileIO) GetWorkingDirectory() (string, error) { return os.Getwd() } + +func (f *FileIO) IsDir(path string) (bool, error) { + pathInfo, err := os.Stat(path) + if err != nil { + return false, err + } + return pathInfo.IsDir(), nil +} + +func (f *FileIO) Walk(path string, callback func(path string, info os.FileInfo, err error) error) error { + return filepath.Walk(path, callback) +} diff --git a/pkg/file/file_test.go b/pkg/file/file_test.go index bf989a0..68b815f 100644 --- a/pkg/file/file_test.go +++ b/pkg/file/file_test.go @@ -1,6 +1,8 @@ package file import ( + "os" + "path/filepath" "testing" ) @@ -8,7 +10,7 @@ func TestResolveRelativeTo(t *testing.T) { t.Run("absolute path", func(t *testing.T) { subject := &FileIO{} expected := "/tmp/foo.yml" - actual, err := subject.ResolveRelativeTo("/tmp/foo.yml", "./bar/other.yml") + actual, err := subject.ResolveRelativeTo("/tmp/foo.yml", "../../test/data") if err != nil { t.Errorf(err.Error()) } @@ -16,7 +18,7 @@ func TestResolveRelativeTo(t *testing.T) { t.Errorf("Expected:\n'''%v'''\nActual:\n'''%v'''\n", expected, actual) } }) - t.Run("relative path", func(t *testing.T) { + t.Run("relative to file", func(t *testing.T) { subject := &FileIO{} expected := "../../test/data/opsfile.yml" actual, err := subject.ResolveRelativeTo("./opsfile.yml", "../../test/data/library.yml") @@ -27,4 +29,53 @@ func TestResolveRelativeTo(t *testing.T) { t.Errorf("Expected:\n'''%v'''\nActual:\n'''%v'''\n", expected, actual) } }) + t.Run("relative to directory", func(t *testing.T) { + subject := &FileIO{} + expected := "../../test/data/opsfile.yml" + actual, err := subject.ResolveRelativeTo("./opsfile.yml", "../../test/data") + if err != nil { + t.Errorf(err.Error()) + } + if expected != actual { + t.Errorf("Expected:\n'''%v'''\nActual:\n'''%v'''\n", expected, actual) + } + }) +} + +func TestResolveRelativeFrom(t *testing.T) { + t.Run("absolute path", func(t *testing.T) { + subject := &FileIO{} + wd, _ := os.Getwd() + absWd, _ := filepath.Abs(wd) + expected := "../../pkg/file/opsfile.yml" + actual, err := subject.ResolveRelativeFrom(filepath.Join(absWd, "opsfile.yml"), "../../test/data") + if err != nil { + t.Errorf(err.Error()) + } + if expected != filepath.Clean(actual) { + t.Errorf("Expected:\n'''%v'''\nActual:\n'''%v'''\n", expected, actual) + } + }) + t.Run("relative to file", func(t *testing.T) { + subject := &FileIO{} + expected := "../../pkg/file/opsfile.yml" + actual, err := subject.ResolveRelativeFrom("./opsfile.yml", "../../test/data/library.yml") + if err != nil { + t.Errorf(err.Error()) + } + if expected != actual { + t.Errorf("Expected:\n'''%v'''\nActual:\n'''%v'''\n", expected, actual) + } + }) + t.Run("relative to directory", func(t *testing.T) { + subject := &FileIO{} + expected := "../../pkg/file/opsfile.yml" + actual, err := subject.ResolveRelativeFrom("./opsfile.yml", "../../test/data") + if err != nil { + t.Errorf(err.Error()) + } + if expected != actual { + t.Errorf("Expected:\n'''%v'''\nActual:\n'''%v'''\n", expected, actual) + } + }) } diff --git a/pkg/importer/importer.go b/pkg/importer/importer.go new file mode 100644 index 0000000..457ade3 --- /dev/null +++ b/pkg/importer/importer.go @@ -0,0 +1,115 @@ +package importer + +import ( + "fmt" + "github.com/cjnosal/manifer/pkg/file" + "github.com/cjnosal/manifer/pkg/interpolator" + "github.com/cjnosal/manifer/pkg/library" + "os" + "path/filepath" + "strings" +) + +type Importer interface { + Import(libType library.Type, path string, recursive bool, outPath string) (*library.Library, error) +} + +type libraryImporter struct { + fileIO file.FileAccess + validator interpolator.Interpolator +} + +func NewImporter(fileIO file.FileAccess, validator interpolator.Interpolator) Importer { + return &libraryImporter{ + fileIO: fileIO, + validator: validator, + } +} + +func (l *libraryImporter) Import(libType library.Type, path string, recursive bool, outPath string) (*library.Library, error) { + lib := &library.Library{ + Type: libType, + } + + isDir, err := l.fileIO.IsDir(path) + if err != nil { + return nil, fmt.Errorf("%w\n checking import path %s", err, path) + } + if isDir { + scenarios, err := l.importDir(path, recursive, outPath) + if err != nil { + return nil, fmt.Errorf("%w\n importing directory %s", err, path) + } + lib.Scenarios = scenarios + } else { + scenario, err := l.importFile(path, filepath.Dir(outPath)) + if err != nil { + return nil, fmt.Errorf("%w\n importing file %s", err, path) + } + lib.Scenarios = []library.Scenario{} + if scenario != nil { + lib.Scenarios = append(lib.Scenarios, *scenario) + } + } + return lib, nil +} + +func (l *libraryImporter) importDir(dirPath string, recursive bool, outPath string) ([]library.Scenario, error) { + scenarios := []library.Scenario{} + + err := l.fileIO.Walk(dirPath, func(path string, info os.FileInfo, err error) error { + if err != nil { + return fmt.Errorf("%w\n walking to %s", err, path) + } + if info.IsDir() { + if recursive { + return nil + } else { + return filepath.SkipDir + } + } + + scenario, err := l.importFile(path, filepath.Dir(outPath)) + if err != nil { + return fmt.Errorf("%w\n importing file %s", err, path) + } + if scenario != nil { + scenarios = append(scenarios, *scenario) + } + return nil + }) + if err != nil { + return nil, fmt.Errorf("%w\n walking directory %s", err, dirPath) + } + return scenarios, nil +} + +func (l *libraryImporter) importFile(path string, outPath string) (*library.Scenario, error) { + valid, err := l.validator.ValidateSnippet(path) + if err != nil { + return nil, fmt.Errorf("%w\n validating file %s", err, path) + } + if !valid { + return nil, nil + } + + base := filepath.Base(path) + ext := filepath.Ext(path) + name := base + if ext != "" { + name = base[0:strings.LastIndex(base, ext)] + } + relPath, err := l.fileIO.ResolveRelativeFrom(path, outPath) + if err != nil { + return nil, fmt.Errorf("%w\n resolving relative path from %s", err, outPath) + } + return &library.Scenario{ + Name: name, + Description: fmt.Sprintf("imported from %s", base), + Snippets: []library.Snippet{ + library.Snippet{ + Path: relPath, + }, + }, + }, nil +} diff --git a/pkg/importer/importer_test.go b/pkg/importer/importer_test.go new file mode 100644 index 0000000..d018835 --- /dev/null +++ b/pkg/importer/importer_test.go @@ -0,0 +1,340 @@ +package importer + +import ( + "errors" + "os" + "path/filepath" + "testing" + "time" + + "github.com/cjnosal/manifer/test" + "github.com/golang/mock/gomock" + "github.com/google/go-cmp/cmp" + + "github.com/cjnosal/manifer/pkg/file" + "github.com/cjnosal/manifer/pkg/interpolator" + "github.com/cjnosal/manifer/pkg/library" +) + +type TestFileInfo struct { + dir bool + name string +} + +func (t *TestFileInfo) Name() string { return t.name } +func (t *TestFileInfo) Size() int64 { return 0 } +func (t *TestFileInfo) Mode() os.FileMode { return 0000 } +func (t *TestFileInfo) ModTime() time.Time { return time.Now() } +func (t *TestFileInfo) IsDir() bool { return t.dir } +func (t *TestFileInfo) Sys() interface{} { return nil } + +func TestImport(t *testing.T) { + t.Run("check dir error", func(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + mockInterpolator := interpolator.NewMockInterpolator(ctrl) + mockFile := file.NewMockFileAccess(ctrl) + subject := NewImporter(mockFile, mockInterpolator) + + mockFile.EXPECT().IsDir("/in").Times(1).Return(false, errors.New("oops")) + + expectedErr := errors.New("oops\n checking import path /in") + _, err := subject.Import(library.OpsFile, "/in", true, "/out") + + if !cmp.Equal(&expectedErr, &err, cmp.Comparer(test.EqualMessage)) { + t.Errorf("Expected:\n'%v'\nActual:\n'%v'\n", expectedErr, err) + } + }) + + t.Run("validate single file error", func(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + mockInterpolator := interpolator.NewMockInterpolator(ctrl) + mockFile := file.NewMockFileAccess(ctrl) + subject := NewImporter(mockFile, mockInterpolator) + + mockFile.EXPECT().IsDir("/in").Times(1).Return(false, nil) + mockInterpolator.EXPECT().ValidateSnippet("/in").Times(1).Return(false, errors.New("oops")) + + expectedErr := errors.New("oops\n validating file /in\n importing file /in") + _, err := subject.Import(library.OpsFile, "/in", true, "/dir/out") + + if !cmp.Equal(&expectedErr, &err, cmp.Comparer(test.EqualMessage)) { + t.Errorf("Expected:\n'%v'\nActual:\n'%v'\n", expectedErr, err) + } + }) + + t.Run("invalid single file", func(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + mockInterpolator := interpolator.NewMockInterpolator(ctrl) + mockFile := file.NewMockFileAccess(ctrl) + subject := NewImporter(mockFile, mockInterpolator) + + mockFile.EXPECT().IsDir("/in").Times(1).Return(false, nil) + mockInterpolator.EXPECT().ValidateSnippet("/in").Times(1).Return(false, nil) + + expectedLib := &library.Library{ + Type: library.OpsFile, + Scenarios: []library.Scenario{}, + } + lib, err := subject.Import(library.OpsFile, "/in", true, "/dir/out") + + if err != nil { + t.Errorf("Unexpected error %v", err) + } + if !cmp.Equal(expectedLib, lib) { + t.Errorf("Expected:\n'%v'\nActual:\n'%v'\n", expectedLib, lib) + } + }) + + t.Run("resolve file path error", func(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + mockInterpolator := interpolator.NewMockInterpolator(ctrl) + mockFile := file.NewMockFileAccess(ctrl) + subject := NewImporter(mockFile, mockInterpolator) + + mockFile.EXPECT().IsDir("/in").Times(1).Return(false, nil) + mockInterpolator.EXPECT().ValidateSnippet("/in").Times(1).Return(true, nil) + mockFile.EXPECT().ResolveRelativeFrom("/in", "/dir").Times(1).Return("", errors.New("oops")) + + expectedErr := errors.New("oops\n resolving relative path from /dir\n importing file /in") + _, err := subject.Import(library.OpsFile, "/in", true, "/dir/out") + + if !cmp.Equal(&expectedErr, &err, cmp.Comparer(test.EqualMessage)) { + t.Errorf("Expected:\n'%v'\nActual:\n'%v'\n", expectedErr, err) + } + }) + + t.Run("import file", func(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + mockInterpolator := interpolator.NewMockInterpolator(ctrl) + mockFile := file.NewMockFileAccess(ctrl) + subject := NewImporter(mockFile, mockInterpolator) + + mockFile.EXPECT().IsDir("/in").Times(1).Return(false, nil) + mockInterpolator.EXPECT().ValidateSnippet("/in").Times(1).Return(true, nil) + mockFile.EXPECT().ResolveRelativeFrom("/in", "/dir").Times(1).Return("../in", nil) + + expectedLib := &library.Library{ + Type: library.OpsFile, + Scenarios: []library.Scenario{ + { + Name: "in", + Description: "imported from in", + Snippets: []library.Snippet{ + library.Snippet{ + Path: "../in", + }, + }, + }, + }, + } + lib, err := subject.Import(library.OpsFile, "/in", true, "/dir/out") + + if err != nil { + t.Errorf("Unexpected error %v", err) + } + if !cmp.Equal(expectedLib, lib) { + t.Errorf("Expected:\n'%v'\nActual:\n'%v'\n", expectedLib, lib) + } + }) + + t.Run("walk dir error", func(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + mockInterpolator := interpolator.NewMockInterpolator(ctrl) + mockFile := file.NewMockFileAccess(ctrl) + subject := NewImporter(mockFile, mockInterpolator) + + mockFile.EXPECT().IsDir("/in").Times(1).Return(true, nil) + mockFile.EXPECT().Walk("/in", gomock.Any()).Times(1).Return(errors.New("oops")) + + expectedErr := errors.New("oops\n walking directory /in\n importing directory /in") + _, err := subject.Import(library.OpsFile, "/in", true, "/dir/out") + + if !cmp.Equal(&expectedErr, &err, cmp.Comparer(test.EqualMessage)) { + t.Errorf("Expected:\n'%v'\nActual:\n'%v'\n", expectedErr, err) + } + }) + + t.Run("walk file error", func(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + mockInterpolator := interpolator.NewMockInterpolator(ctrl) + mockFile := file.NewMockFileAccess(ctrl) + subject := NewImporter(mockFile, mockInterpolator) + + mockFile.EXPECT().IsDir("/in").Times(1).Return(true, nil) + mockFile.EXPECT().Walk("/in", gomock.Any()).Times(1).Do(func(path string, callback func(path string, info os.FileInfo, err error) error) error { + err := callback("f", nil, errors.New("oops")) + expectedErr := errors.New("oops\n walking to f") + if !cmp.Equal(&expectedErr, &err, cmp.Comparer(test.EqualMessage)) { + t.Errorf("Expected:\n'%v'\nActual:\n'%v'\n", expectedErr, err) + } + return err + }) + + subject.Import(library.OpsFile, "/in", true, "/dir/out") + }) + + t.Run("non-recursive skips dir", func(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + mockInterpolator := interpolator.NewMockInterpolator(ctrl) + mockFile := file.NewMockFileAccess(ctrl) + subject := NewImporter(mockFile, mockInterpolator) + + mockFile.EXPECT().IsDir("/in").Times(1).Return(true, nil) + mockFile.EXPECT().Walk("/in", gomock.Any()).Times(1).Do(func(path string, callback func(path string, info os.FileInfo, err error) error) error { + err := callback("f", &TestFileInfo{dir: true}, nil) + expectedErr := filepath.SkipDir + if !cmp.Equal(&expectedErr, &err, cmp.Comparer(test.EqualMessage)) { + t.Errorf("Expected:\n'%v'\nActual:\n'%v'\n", expectedErr, err) + } + return err + }) + + subject.Import(library.OpsFile, "/in", false, "/dir/out") + }) + + t.Run("non-recursive skips dir", func(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + mockInterpolator := interpolator.NewMockInterpolator(ctrl) + mockFile := file.NewMockFileAccess(ctrl) + subject := NewImporter(mockFile, mockInterpolator) + + mockFile.EXPECT().IsDir("/in").Times(1).Return(true, nil) + mockFile.EXPECT().Walk("/in", gomock.Any()).Times(1).Do(func(path string, callback func(path string, info os.FileInfo, err error) error) error { + err := callback("f", &TestFileInfo{dir: true}, nil) + if err != nil { + t.Errorf("Unexpected error %v", err) + } + return err + }) + + subject.Import(library.OpsFile, "/in", true, "/dir/out") + }) + + t.Run("validate file in dir error", func(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + mockInterpolator := interpolator.NewMockInterpolator(ctrl) + mockFile := file.NewMockFileAccess(ctrl) + subject := NewImporter(mockFile, mockInterpolator) + + mockFile.EXPECT().IsDir("/in").Times(1).Return(true, nil) + mockInterpolator.EXPECT().ValidateSnippet("f").Times(1).Return(false, errors.New("oops")) + mockFile.EXPECT().Walk("/in", gomock.Any()).Times(1).Do(func(path string, callback func(path string, info os.FileInfo, err error) error) error { + err := callback("f", &TestFileInfo{dir: false}, nil) + expectedErr := errors.New("oops\n validating file f\n importing file f") + if !cmp.Equal(&expectedErr, &err, cmp.Comparer(test.EqualMessage)) { + t.Errorf("Expected:\n'%v'\nActual:\n'%v'\n", expectedErr, err) + } + return err + }) + + subject.Import(library.OpsFile, "/in", false, "/dir/out") + }) + + t.Run("resolve file path in dir error", func(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + mockInterpolator := interpolator.NewMockInterpolator(ctrl) + mockFile := file.NewMockFileAccess(ctrl) + subject := NewImporter(mockFile, mockInterpolator) + + mockFile.EXPECT().IsDir("/in").Times(1).Return(true, nil) + mockInterpolator.EXPECT().ValidateSnippet("f").Times(1).Return(true, nil) + mockFile.EXPECT().ResolveRelativeFrom("f", "/dir").Times(1).Return("", errors.New("oops")) + + mockFile.EXPECT().Walk("/in", gomock.Any()).Times(1).Do(func(path string, callback func(path string, info os.FileInfo, err error) error) error { + err := callback("f", &TestFileInfo{dir: false}, nil) + expectedErr := errors.New("oops\n resolving relative path from /dir\n importing file f") + if !cmp.Equal(&expectedErr, &err, cmp.Comparer(test.EqualMessage)) { + t.Errorf("Expected:\n'%v'\nActual:\n'%v'\n", expectedErr, err) + } + return err + }) + + subject.Import(library.OpsFile, "/in", false, "/dir/out") + }) + + t.Run("import opsfiles from directory", func(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + mockInterpolator := interpolator.NewMockInterpolator(ctrl) + mockFile := file.NewMockFileAccess(ctrl) + subject := NewImporter(mockFile, mockInterpolator) + + mockFile.EXPECT().IsDir("/in").Times(1).Return(true, nil) + mockInterpolator.EXPECT().ValidateSnippet("f").Times(1).Return(true, nil) + mockInterpolator.EXPECT().ValidateSnippet("g").Times(1).Return(false, nil) + mockInterpolator.EXPECT().ValidateSnippet("h").Times(1).Return(true, nil) + mockFile.EXPECT().ResolveRelativeFrom("f", "/dir").Times(1).Return("../f", nil) + mockFile.EXPECT().ResolveRelativeFrom("h", "/dir").Times(1).Return("../h", nil) + + mockFile.EXPECT().Walk("/in", gomock.Any()).Times(1).Do(func(path string, callback func(path string, info os.FileInfo, err error) error) error { + err := callback("f", &TestFileInfo{dir: false}, nil) + if err != nil { + t.Errorf("Unexpected error %v", err) + } + err = callback("g", &TestFileInfo{dir: false}, nil) + if err != nil { + t.Errorf("Unexpected error %v", err) + } + err = callback("h", &TestFileInfo{dir: false}, nil) + if err != nil { + t.Errorf("Unexpected error %v", err) + } + return nil + }) + + expectedLib := &library.Library{ + Type: library.OpsFile, + Scenarios: []library.Scenario{ + { + Name: "f", + Description: "imported from f", + Snippets: []library.Snippet{ + library.Snippet{ + Path: "../f", + }, + }, + }, + { + Name: "h", + Description: "imported from h", + Snippets: []library.Snippet{ + library.Snippet{ + Path: "../h", + }, + }, + }, + }, + } + lib, err := subject.Import(library.OpsFile, "/in", false, "/dir/out") + + if err != nil { + t.Errorf("Unexpected error %v", err) + } + if !cmp.Equal(expectedLib, lib) { + t.Errorf("Expected:\n'%v'\nActual:\n'%v'\n", expectedLib, lib) + } + }) +} diff --git a/pkg/interpolator/interpolator.go b/pkg/interpolator/interpolator.go index 26e54bf..ee4f58b 100644 --- a/pkg/interpolator/interpolator.go +++ b/pkg/interpolator/interpolator.go @@ -6,6 +6,7 @@ import ( ) type Interpolator interface { + ValidateSnippet(path string) (bool, error) ParsePassthroughFlags(templateArgs []string) (*library.ScenarioNode, error) Interpolate(template *file.TaggedBytes, snippet *file.TaggedBytes, snippetArgs []string, templateArgs []string) ([]byte, error) } diff --git a/pkg/interpolator/opsfile/opsfile.go b/pkg/interpolator/opsfile/opsfile.go index 068acba..6eac361 100644 --- a/pkg/interpolator/opsfile/opsfile.go +++ b/pkg/interpolator/opsfile/opsfile.go @@ -13,21 +13,25 @@ import ( "github.com/jessevdk/go-flags" ) -func NewOpsFileInterpolator(y yaml.YamlAccess) interpolator.Interpolator { +func NewOpsFileInterpolator(y yaml.YamlAccess, f file.FileAccess) interpolator.Interpolator { i := &ofInt{ Yaml: y, + File: f, } return &interpolatorWrapper{ interpolator: i, + file: f, } } type interpolatorWrapper struct { interpolator opFileInterpolator + file file.FileAccess } type ofInt struct { Yaml yaml.YamlAccess + File file.FileAccess } type opFileInterpolator interface { @@ -39,6 +43,16 @@ type opFlags struct { Oppaths []string `long:"ops-file" short:"o" value-name:"PATH" description:"Load manifest operations from a YAML file"` } +func (i *interpolatorWrapper) ValidateSnippet(path string) (bool, error) { + content, err := i.file.Read(path) + if err != nil { + return false, fmt.Errorf("%w\n while validating opsfile %s", err, path) + } + opDefs := []patch.OpDefinition{} + err = i.interpolator.(*ofInt).Yaml.Unmarshal(content, &opDefs) + return err == nil, nil +} + func (i *interpolatorWrapper) ParsePassthroughFlags(args []string) (*library.ScenarioNode, error) { var node *library.ScenarioNode if len(args) > 0 { diff --git a/pkg/interpolator/opsfile/opsfile_test.go b/pkg/interpolator/opsfile/opsfile_test.go index a9885c9..89cab39 100644 --- a/pkg/interpolator/opsfile/opsfile_test.go +++ b/pkg/interpolator/opsfile/opsfile_test.go @@ -104,6 +104,77 @@ func TestParsePassthroughFlags(t *testing.T) { }) } +func TestValidate(t *testing.T) { + t.Run("valid ops file", func(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + mockYaml := yaml.NewMockYamlAccess(ctrl) + mockFile := file.NewMockFileAccess(ctrl) + + subject := NewOpsFileInterpolator(mockYaml, mockFile) + + mockFile.EXPECT().Read("/foo").Times(1).Return([]byte{1}, nil) + mockYaml.EXPECT().Unmarshal([]byte{1}, &[]patch.OpDefinition{}).Times(1).Return(nil) + + valid, err := subject.ValidateSnippet("/foo") + + if err != nil { + t.Errorf("Unexpected error %v", err) + } + + if !valid { + t.Errorf("Expected ValidateSnippet to return true") + } + }) + + t.Run("invalid ops file", func(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + mockYaml := yaml.NewMockYamlAccess(ctrl) + mockFile := file.NewMockFileAccess(ctrl) + + subject := NewOpsFileInterpolator(mockYaml, mockFile) + + mockFile.EXPECT().Read("/foo").Times(1).Return([]byte{1}, nil) + mockYaml.EXPECT().Unmarshal([]byte{1}, &[]patch.OpDefinition{}).Times(1).Return(errors.New("oops")) + + valid, err := subject.ValidateSnippet("/foo") + + if err != nil { + t.Errorf("Unexpected error %v", err) + } + + if valid { + t.Errorf("Expected ValidateSnippet to return false") + } + }) + + t.Run("file error", func(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + mockYaml := yaml.NewMockYamlAccess(ctrl) + mockFile := file.NewMockFileAccess(ctrl) + + subject := NewOpsFileInterpolator(mockYaml, mockFile) + + mockFile.EXPECT().Read("/foo").Times(1).Return(nil, errors.New("oops")) + + valid, err := subject.ValidateSnippet("/foo") + + expectedError := errors.New("oops\n while validating opsfile /foo") + if !cmp.Equal(&expectedError, &err, cmp.Comparer(test.EqualMessage)) { + t.Errorf("Expected error:\n'''%s'''\nActual:\n'''%s'''\n", expectedError, err) + } + + if valid { + t.Errorf("Expected ValidateSnippet to return false") + } + }) +} + func TestWrapper(t *testing.T) { cases := []struct { name string