From c4a7cd70abedabd9fed0dfc31a4c692f12bf59f9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20H=C3=A4usler?= <794584+corvus-ch@users.noreply.github.com> Date: Mon, 29 Aug 2022 17:22:05 +0200 Subject: [PATCH] Add format sub command (#24) --- format/action.go | 43 +++++++++++ format/action_test.go | 90 +++++++++++++++++++++++ format/config.go | 14 ++++ format/config_test.go | 49 ++++++++++++ internal/app.go | 2 + internal/app_test.go | 1 + internal/create_test.go | 48 ++---------- internal/fixtures/TestApp/default.golden | 3 + internal/fixtures/TestApp/format.golden | 13 ++++ internal/format.go | 94 ++++++++++++++++++++++++ internal/format_test.go | 47 ++++++++++++ internal/utils_test.go | 84 +++++++++++++++++++++ 12 files changed, 447 insertions(+), 41 deletions(-) create mode 100644 format/action.go create mode 100644 format/action_test.go create mode 100644 format/config.go create mode 100644 format/config_test.go create mode 100644 internal/fixtures/TestApp/format.golden create mode 100644 internal/format.go create mode 100644 internal/format_test.go create mode 100644 internal/utils_test.go diff --git a/format/action.go b/format/action.go new file mode 100644 index 0000000..ecc795b --- /dev/null +++ b/format/action.go @@ -0,0 +1,43 @@ +package format + +import ( + "fmt" + "io" + + "github.com/bketelsen/logr" +) + +// DoFormat applies a format to a given input. +func DoFormat(cfg Config, log logr.Logger) (result error) { + reader, err := cfg.Input() + if err != nil { + return fmt.Errorf("failed to open input: %v", err) + } + + if closer, ok := reader.(io.Closer); ok { + defer closer.Close() + } + + formats, err := cfg.Formats() + if err != nil { + return fmt.Errorf("failed to setup output formatting: %v", err) + } + + factory := NewFactory(formats, false, log) + defer func() { + if err := factory.Close(); err != nil && result == nil { + result = err + } + }() + + writer, err := factory.Create(0) + if err != nil { + return fmt.Errorf("failed to setup output writer: %v", err) + } + + if _, err := io.Copy(writer, reader); nil != err { + result = fmt.Errorf("failed to process data: %v", err) + } + + return +} diff --git a/format/action_test.go b/format/action_test.go new file mode 100644 index 0000000..a4c19f7 --- /dev/null +++ b/format/action_test.go @@ -0,0 +1,90 @@ +package format_test + +import ( + "bytes" + "fmt" + "io/ioutil" + "os" + "path/filepath" + "testing" + + "github.com/corvus-ch/horcrux/format" + "github.com/corvus-ch/horcrux/format/raw" + "github.com/corvus-ch/horcrux/input" + "github.com/corvus-ch/logr/buffered" + "github.com/pkg/errors" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" +) + +func TestFormat(t *testing.T) { + dir := createDir(t) + defer os.RemoveAll(dir) + + f := raw.New(input.NewStreamInput(outputStem(dir, raw.Name))) + cfg := NewConfig(t.Name(), f) + log := buffered.New(1) + + err := format.DoFormat(cfg, log) + + assert.Nil(t, err) + mock.AssertExpectationsForObjects(t, cfg) + + files, _ := filepath.Glob(fmt.Sprintf("%s.*", outputStem(dir, raw.Name))) + + assert.Empty(t, log.Buf()) + assert.Len(t, files, 1) +} + +var errorTests = []struct { + name string + setup func(t *testing.T, cfg *Config, dir string) + output string +}{ + {"input", func(t *testing.T, cfg *Config, _ string) { + cfg.On("Input").Maybe().Return(nil, errors.New("input error")) + }, "failed to open input: input error"}, + + {"formats", func(t *testing.T, cfg *Config, _ string) { + cfg.On("Input").Return(bytes.NewBufferString(t.Name()), nil) + cfg.On("Formats").Return(nil, errors.New("format error")) + }, "failed to setup output formatting: format error"}, + + {"copy", func(t *testing.T, cfg *Config, dir string) { + f, _ := os.Open(t.Name()) + cfg.On("Input").Return(f, nil) + cfg.On("InputInfo").Maybe().Return(input.NewStreamInput(outputStem(dir, raw.Name))) + cfg.On("Formats").Maybe().Return([]format.Format{raw.New(input.NewStreamInput(outputStem(dir, raw.Name)))}, nil) + }, "failed to process data: invalid argument"}, +} + +func TestErrors(t *testing.T) { + for _, test := range errorTests { + t.Run(test.name, func(t *testing.T) { + dir := createDir(t) + defer os.RemoveAll(dir) + cfg := &Config{} + log := buffered.New(1) + test.setup(t, cfg, dir) + + err := format.DoFormat(cfg, log) + + assert.Error(t, err, test.output) + assert.Empty(t, log.Buf()) + mock.AssertExpectationsForObjects(t, cfg) + }) + } +} + +func createDir(t *testing.T) string { + dir, err := ioutil.TempDir("", "horcrux_create") + if err != nil { + t.Error(err) + } + + return dir +} + +func outputStem(dir, format string) string { + return fmt.Sprintf("%s/%s", dir, format) +} diff --git a/format/config.go b/format/config.go new file mode 100644 index 0000000..3fe47c7 --- /dev/null +++ b/format/config.go @@ -0,0 +1,14 @@ +package format + +import ( + "io" + + "github.com/corvus-ch/horcrux/input" +) + +// Config define the configuration input required for creating an output format. +type Config interface { + Input() (io.Reader, error) + InputInfo() input.Input + Formats() ([]Format, error) +} diff --git a/format/config_test.go b/format/config_test.go new file mode 100644 index 0000000..19f8c51 --- /dev/null +++ b/format/config_test.go @@ -0,0 +1,49 @@ +package format_test + +import ( + "bytes" + "io" + + "github.com/corvus-ch/horcrux/format" + "github.com/corvus-ch/horcrux/input" + "github.com/stretchr/testify/mock" +) + +type Config struct { + mock.Mock +} + +func NewConfig(name string, f format.Format) *Config { + cfg := &Config{} + cfg.On("Input").Maybe().Return(bytes.NewBufferString(name), nil) + cfg.On("InputInfo").Maybe().Return(input.NewStreamInput("")) + cfg.On("Formats").Maybe().Return([]format.Format{f}, nil) + + return cfg +} + +func (c *Config) Input() (io.Reader, error) { + args := c.Called() + r := args.Get(0) + if r == nil { + return nil, args.Error(1) + } + + return r.(io.Reader), args.Error(1) +} + +func (c *Config) InputInfo() input.Input { + args := c.Called() + + return args.Get(0).(input.Input) +} + +func (c *Config) Formats() ([]format.Format, error) { + args := c.Called() + f := args.Get(0) + if f == nil { + return nil, args.Error(1) + } + + return f.([]format.Format), args.Error(1) +} diff --git a/internal/app.go b/internal/app.go index 8e858d7..9b7d706 100644 --- a/internal/app.go +++ b/internal/app.go @@ -3,6 +3,7 @@ package internal import ( "github.com/bketelsen/logr" "github.com/corvus-ch/horcrux/create" + "github.com/corvus-ch/horcrux/format" "github.com/corvus-ch/horcrux/restore" "github.com/corvus-ch/logr/writer_adapter" "gopkg.in/alecthomas/kingpin.v2" @@ -14,6 +15,7 @@ func App(log logr.Logger) *kingpin.Application { app.UsageWriter(w) app.ErrorWriter(w) RegisterCreateCommand(app, log, create.Create) + RegisterFormatCommand(app, log, format.DoFormat) RegisterRestoreCommand(app, log, restore.Restore) return app diff --git a/internal/app_test.go b/internal/app_test.go index 57f1e00..d60defd 100644 --- a/internal/app_test.go +++ b/internal/app_test.go @@ -15,6 +15,7 @@ var appTests = []struct { }{ {"default", []string{"help"}}, {"create", []string{"help", "create"}}, + {"format", []string{"help", "format"}}, {"restore", []string{"help", "restore"}}, } diff --git a/internal/create_test.go b/internal/create_test.go index a8b5c53..4c8cf6e 100644 --- a/internal/create_test.go +++ b/internal/create_test.go @@ -1,15 +1,11 @@ package internal_test import ( - "io/ioutil" "os" "testing" "github.com/bketelsen/logr" "github.com/corvus-ch/horcrux/create" - "github.com/corvus-ch/horcrux/format/raw" - "github.com/corvus-ch/horcrux/format/text" - "github.com/corvus-ch/horcrux/format/zbase32" "github.com/corvus-ch/horcrux/internal" "github.com/corvus-ch/logr/buffered" "github.com/stretchr/testify/assert" @@ -82,25 +78,13 @@ func TestCreateCommand_Parts(t *testing.T) { } func TestCreateCommand_Input(t *testing.T) { - file, err := ioutil.TempFile("", t.Name()) - if err != nil { - t.Fatal(err) - } + file := tmpFile(t) defer os.Remove(file.Name()) - tests := []struct { - name string - args []string - file *os.File - }{ - {"default", []string{"create"}, os.Stdin}, - {"stdin", []string{"create", "--", "-"}, os.Stdin}, - {"file", []string{"create", "--", file.Name()}, file}, - } - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { + for name, test := range newInputTests("create", file) { + t.Run(name, func(t *testing.T) { assertCreateAction(t, test.args, func(cfg create.Config, _ logr.Logger) error { reader, err := cfg.Input() - assert.Nil(t, err) + assert.NoError(t, err) assert.Equal(t, test.file.Name(), reader.(*os.File).Name()) return nil }) @@ -109,28 +93,10 @@ func TestCreateCommand_Input(t *testing.T) { } func TestCreateCommand_Formats(t *testing.T) { - tests := []struct { - name string - args []string - formats []string - }{ - {"default", []string{"create"}, []string{text.Name}}, - {"single", []string{"create", "-f", "raw"}, []string{raw.Name}}, - {"multiple", []string{"create", "-f", "raw", "-f", "zbase32"}, []string{raw.Name, zbase32.Name}}, - } - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { + for name, test := range newFormatsTests("create") { + t.Run(name, func(t *testing.T) { assertCreateAction(t, test.args, func(cfg create.Config, _ logr.Logger) error { - formats, err := cfg.Formats() - if err != nil { - t.Fatal(err) - } - if len(test.formats) != len(formats) { - t.Fatalf("expected %d formats, got %d", len(test.formats), len(formats)) - } - for i, name := range test.formats { - assert.Equal(t, name, formats[i].Name()) - } + assertFormats(t, cfg, test.formats) return nil }) }) diff --git a/internal/fixtures/TestApp/default.golden b/internal/fixtures/TestApp/default.golden index a10d64f..60b9125 100644 --- a/internal/fixtures/TestApp/default.golden +++ b/internal/fixtures/TestApp/default.golden @@ -12,6 +12,9 @@ ERROR ERROR create [] [] ERROR create a new set of horcruxes ERROR +ERROR format [] [] +ERROR formats input the same way as create would do without doing the split +ERROR ERROR restore [] ... ERROR restores your valuable data from a set of horcruxes ERROR diff --git a/internal/fixtures/TestApp/format.golden b/internal/fixtures/TestApp/format.golden new file mode 100644 index 0000000..a8c3154 --- /dev/null +++ b/internal/fixtures/TestApp/format.golden @@ -0,0 +1,13 @@ +ERROR usage: horcrux format [] [] +ERROR +ERROR formats input the same way as create would do without doing the split +ERROR +ERROR Flags: +ERROR --help Show context-sensitive help (also try --help-long and +ERROR --help-man). +ERROR -f, --format=text ... the output formats +ERROR -o, --output=OUTPUT name stem for the output files +ERROR +ERROR Args: +ERROR [] the input file +ERROR diff --git a/internal/format.go b/internal/format.go new file mode 100644 index 0000000..fcc3217 --- /dev/null +++ b/internal/format.go @@ -0,0 +1,94 @@ +package internal + +import ( + "io" + "os" + + "github.com/bketelsen/logr" + "github.com/corvus-ch/horcrux/format" + "github.com/corvus-ch/horcrux/input" + "gopkg.in/alecthomas/kingpin.v2" +) + +type formatAction func(cfg format.Config, logger logr.Logger) error + +type formatCommand struct { + action formatAction + + // The logger + log logr.Logger + + // Arguments + input string + + // Flags + formats []string + stemFlag string + + // internal + info input.Input +} + +// RegisterFormatCommand registers the format sub command with the application. +func RegisterFormatCommand(app *kingpin.Application, log logr.Logger, action formatAction) *formatCommand { + c := &formatCommand{action: action, log: log} + + cc := app.Command("format", "formats input the same way as create would do without doing the split") + cc.Action(c.Execute) + cc.Arg("input", "the input file"). + StringVar(&c.input) + cc.Flag("format", "the output formats"). + Default(format.Default). + Short('f'). + StringsVar(&c.formats) + cc.Flag("output", "name stem for the output files"). + Short('o'). + StringVar(&c.stemFlag) + + return c +} + +// Execute runs the action callback. +func (c *formatCommand) Execute(_ *kingpin.ParseContext) error { + return c.action(c, c.log) +} + +// Input returns the input file. +func (c *formatCommand) Input() (io.Reader, error) { + if c.input == "-" || c.input == "" { + return os.Stdin, nil + } + + return os.Open(c.input) +} + +// InputInfo returns detail infor about the input. +func (c *formatCommand) InputInfo() input.Input { + if c.info != nil { + return c.info + } + + if c.input == "-" || c.input == "" { + c.info = input.NewStreamInput(c.stemFlag) + } else { + + c.info = input.NewFileInput(c.input, c.stemFlag) + } + + return c.info +} + +// Formats returns the list of output formats to produce +func (c *formatCommand) Formats() ([]format.Format, error) { + formats := make([]format.Format, len(c.formats)) + + for i, f := range c.formats { + ff, err := format.New(f, c.InputInfo()) + if err != nil { + return nil, err + } + formats[i] = ff + } + + return formats, nil +} diff --git a/internal/format_test.go b/internal/format_test.go new file mode 100644 index 0000000..6a69b1e --- /dev/null +++ b/internal/format_test.go @@ -0,0 +1,47 @@ +package internal_test + +import ( + "os" + "testing" + + "github.com/bketelsen/logr" + "github.com/corvus-ch/horcrux/format" + "github.com/corvus-ch/horcrux/internal" + "github.com/corvus-ch/logr/buffered" + "github.com/stretchr/testify/assert" + "gopkg.in/alecthomas/kingpin.v2" +) + +func assertFormatAction(t *testing.T, args []string, action func(format.Config, logr.Logger) error) { + log := buffered.New(0) + app := kingpin.New("test", "test") + internal.RegisterFormatCommand(app, log, action) + _, err := app.Parse(args) + assert.Nil(t, err) +} + +func TestFormatCommand_Input(t *testing.T) { + file := tmpFile(t) + defer os.Remove(file.Name()) + for name, test := range newInputTests("format", file) { + t.Run(name, func(t *testing.T) { + assertFormatAction(t, test.args, func(cfg format.Config, _ logr.Logger) error { + reader, err := cfg.Input() + assert.NoError(t, err) + assert.Equal(t, test.file.Name(), reader.(*os.File).Name()) + return nil + }) + }) + } +} + +func TestFormatCommand_Formats(t *testing.T) { + for name, test := range newFormatsTests("format") { + t.Run(name, func(t *testing.T) { + assertFormatAction(t, test.args, func(cfg format.Config, _ logr.Logger) error { + assertFormats(t, cfg, test.formats) + return nil + }) + }) + } +} diff --git a/internal/utils_test.go b/internal/utils_test.go new file mode 100644 index 0000000..d27b73e --- /dev/null +++ b/internal/utils_test.go @@ -0,0 +1,84 @@ +package internal_test + +import ( + "io/ioutil" + "os" + "testing" + + "github.com/bketelsen/logr" + "github.com/corvus-ch/horcrux/format" + "github.com/corvus-ch/horcrux/format/raw" + "github.com/corvus-ch/horcrux/format/text" + "github.com/corvus-ch/horcrux/format/zbase32" + "github.com/stretchr/testify/assert" +) + +type inputTests map[string]struct { + args []string + file *os.File +} + +type formatTests map[string]struct { + args []string + formats []string +} + +func newInputTests(action string, file *os.File) inputTests { + return inputTests{ + "default": {[]string{action}, os.Stdin}, + "stdin": {[]string{action, "--", "-"}, os.Stdin}, + "file": {[]string{action, "--", file.Name()}, file}, + } +} + +func newFormatsTests(action string) formatTests { + return formatTests{ + "default": {[]string{action}, []string{text.Name}}, + "single": {[]string{action, "-f", "raw"}, []string{raw.Name}}, + "multiple": {[]string{action, "-f", "raw", "-f", "zbase32"}, []string{raw.Name, zbase32.Name}}, + } +} + +func tmpFile(t *testing.T) *os.File { + file, err := ioutil.TempFile("", t.Name()) + if err != nil { + t.Fatal(err) + } + + return file +} + +func assertFormats(t *testing.T, cfg format.Config, testFormats []string) { + formats, err := cfg.Formats() + assert.NoError(t, err) + assert.Equal(t, len(testFormats), len(formats)) + for i, name := range testFormats { + assert.Equal(t, name, formats[i].Name()) + } +} + +func inputTest(t *testing.T, action string) { + file := tmpFile(t) + defer os.Remove(file.Name()) + for name, test := range newInputTests(action, file) { + t.Run(name, func(t *testing.T) { + assertFormatAction(t, test.args, func(cfg format.Config, _ logr.Logger) error { + reader, err := cfg.Input() + assert.NoError(t, err) + assert.Equal(t, test.file.Name(), reader.(*os.File).Name()) + return nil + }) + }) + } +} + +func formatTest(t *testing.T, action string) { + for name, test := range newFormatsTests(action) { + t.Run(name, func(t *testing.T) { + assertFormatAction(t, test.args, func(cfg format.Config, _ logr.Logger) error { + assertFormats(t, cfg, test.formats) + return nil + }) + }) + } +}