From 6987d24609a5d2cca67729899f4b760b7145941c Mon Sep 17 00:00:00 2001 From: david Date: Thu, 21 Feb 2019 21:13:29 -0500 Subject: [PATCH 01/12] implement execution if exec ref from config --- cmd/run.go | 5 ++++- internal/testutil/testutils.go | 15 +++++++++++++++ pkg/summon/options.go | 18 ++++++++++++++++++ pkg/summon/run.go | 18 +++++++++++++++++- 4 files changed, 54 insertions(+), 2 deletions(-) diff --git a/cmd/run.go b/cmd/run.go index d722e09..815027e 100644 --- a/cmd/run.go +++ b/cmd/run.go @@ -7,6 +7,7 @@ import ( type runCmdOpts struct { driver summon.Interface + ref string } func newRunCmd(driver summon.Interface) *cobra.Command { @@ -16,7 +17,9 @@ func newRunCmd(driver summon.Interface) *cobra.Command { rcmd := &cobra.Command{ Use: "run", Short: "launch executable from summonables", + Args: cobra.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { + runCmd.ref = args[0] return runCmd.run() }, } @@ -25,7 +28,7 @@ func newRunCmd(driver summon.Interface) *cobra.Command { } func (r *runCmdOpts) run() error { - r.driver.Configure() + r.driver.Configure(summon.Ref(r.ref)) return r.driver.Run() } diff --git a/internal/testutil/testutils.go b/internal/testutil/testutils.go index 8692b99..b7c6cba 100644 --- a/internal/testutil/testutils.go +++ b/internal/testutil/testutils.go @@ -1,6 +1,9 @@ package testutil import ( + "os" + "os/exec" + "github.com/spf13/afero" ) @@ -18,3 +21,15 @@ func ReplaceFs() func() { SetFs(oldFs) } } + +// FakeExecCommand resturns a fake function which calls into testToCall +// this is used to mock an exec.Cmd +func FakeExecCommand(testToCall string) func(string, ...string) *exec.Cmd { + return func(command string, args ...string) *exec.Cmd { + cs := []string{"-test.run=" + testToCall, "--", command} + cs = append(cs, args...) + cmd := exec.Command(os.Args[0], cs...) + cmd.Env = []string{"GO_WANT_HELPER_PROCESS=1"} + return cmd + } +} diff --git a/pkg/summon/options.go b/pkg/summon/options.go index 690086c..5fe2c13 100644 --- a/pkg/summon/options.go +++ b/pkg/summon/options.go @@ -12,12 +12,30 @@ type options struct { filename string // show tree of files tree bool + // reference to an exec config entry + ref string + + args []string } // Option allows specifying configuration settings // from the user type Option func(*options) +// Args captures the arguments to be passed to run +func Args(args ...string) Option { + return func(opts *options) { + opts.args = args + } +} + +// Ref references an exec config entry +func Ref(ref string) Option { + return func(opts *options) { + opts.ref = ref + } +} + // All specifies to download all config files func All(all bool) Option { return func(opts *options) { diff --git a/pkg/summon/run.go b/pkg/summon/run.go index 2f82b9a..8b5641f 100644 --- a/pkg/summon/run.go +++ b/pkg/summon/run.go @@ -1,6 +1,22 @@ package summon +import "os/exec" + +var execCommand = exec.Command + // Run will run go or executable scripts in the context of the data func (s *Summoner) Run(opts ...Option) error { - return nil + var executor string + var command string + + for k, v := range s.config.Executables { + if c, ok := v[s.opts.ref]; ok { + executor = k + command = c + } + } + _ = executor + _ = command + //cmd := execCommand(executor, append([]string{command}, s.opts.args)) + return nil // cmd.Run() } From 5cdb6f94c559d2f1d4a10bf77ad64d0bc708416f Mon Sep 17 00:00:00 2001 From: david Date: Sat, 23 Feb 2019 11:30:46 -0500 Subject: [PATCH 02/12] implement run command --- internal/testutil/testutils.go | 26 +++++++++++++++++- pkg/summon/run.go | 17 ++++++++---- pkg/summon/run_test.go | 38 ++++++++++++++++++++++++++ pkg/summon/testdata/summon.config.yaml | 7 ++++- 4 files changed, 81 insertions(+), 7 deletions(-) create mode 100644 pkg/summon/run_test.go diff --git a/internal/testutil/testutils.go b/internal/testutil/testutils.go index b7c6cba..068e87f 100644 --- a/internal/testutil/testutils.go +++ b/internal/testutil/testutils.go @@ -1,6 +1,8 @@ package testutil import ( + "fmt" + "io" "os" "os/exec" @@ -24,12 +26,34 @@ func ReplaceFs() func() { // FakeExecCommand resturns a fake function which calls into testToCall // this is used to mock an exec.Cmd -func FakeExecCommand(testToCall string) func(string, ...string) *exec.Cmd { +// Adapted from https://npf.io/2015/06/testing-exec-command/ +func FakeExecCommand(testToCall string, stdout, stderr io.Writer) func(string, ...string) *exec.Cmd { return func(command string, args ...string) *exec.Cmd { cs := []string{"-test.run=" + testToCall, "--", command} cs = append(cs, args...) cmd := exec.Command(os.Args[0], cs...) + + cmd.Stdout = stdout + cmd.Stderr = stderr cmd.Env = []string{"GO_WANT_HELPER_PROCESS=1"} return cmd } } + +// CleanHelperArgs removes the helper process arguments +func CleanHelperArgs(helperArgs []string) []string { + args := os.Args + for len(args) > 0 { + if args[0] == "--" { + args = args[1:] + break + } + args = args[1:] + } + if len(args) == 0 { + fmt.Fprintf(os.Stderr, "No command\n") + os.Exit(2) + } + + return args +} diff --git a/pkg/summon/run.go b/pkg/summon/run.go index 8b5641f..97f0f8e 100644 --- a/pkg/summon/run.go +++ b/pkg/summon/run.go @@ -1,6 +1,8 @@ package summon -import "os/exec" +import ( + "os/exec" +) var execCommand = exec.Command @@ -13,10 +15,15 @@ func (s *Summoner) Run(opts ...Option) error { if c, ok := v[s.opts.ref]; ok { executor = k command = c + break } } - _ = executor - _ = command - //cmd := execCommand(executor, append([]string{command}, s.opts.args)) - return nil // cmd.Run() + + finalCommand := append([]string{command}, s.opts.args...) + + cmd := execCommand(executor, finalCommand...) + + err := cmd.Run() + + return err } diff --git a/pkg/summon/run_test.go b/pkg/summon/run_test.go new file mode 100644 index 0000000..f47dc73 --- /dev/null +++ b/pkg/summon/run_test.go @@ -0,0 +1,38 @@ +package summon + +import ( + "bytes" + "fmt" + "os" + "os/exec" + "strings" + "testing" + + "github.com/davidovich/summon/internal/testutil" + "github.com/gobuffalo/packr/v2" + "github.com/stretchr/testify/assert" +) + +func TestRun(t *testing.T) { + stdout := &bytes.Buffer{} + execCommand = testutil.FakeExecCommand("TestSummonRunHelper", stdout, nil) + defer func() { execCommand = exec.Command }() + + box := packr.New("test run box", "testdata") + + s := New(box, Ref("hello")) + err := s.Run() + + assert.Nil(t, err) + assert.Equal(t, "python -c print(\"hello from python!\")", stdout.String()) +} + +func TestSummonRunHelper(t *testing.T) { + if os.Getenv("GO_WANT_HELPER_PROCESS") != "1" { + return + } + defer os.Exit(0) + + args := testutil.CleanHelperArgs(os.Args) + fmt.Fprintf(os.Stdout, strings.Join(args, " ")) +} diff --git a/pkg/summon/testdata/summon.config.yaml b/pkg/summon/testdata/summon.config.yaml index f6e3489..a166c44 100644 --- a/pkg/summon/testdata/summon.config.yaml +++ b/pkg/summon/testdata/summon.config.yaml @@ -3,4 +3,9 @@ aliases: {} outputdir: "overriden_dir" exec: bash: - hello-bash: hello.sh \ No newline at end of file + hello-bash: hello.sh + go: + gobin: github.com/myitcv/gobin + gohack: github.com/rogppepe/gohack + python -c: + hello: print("hello from python!") From 6ba496be1bfd26d99880794f53f11049f459c8b8 Mon Sep 17 00:00:00 2001 From: david Date: Mon, 25 Feb 2019 21:21:19 -0500 Subject: [PATCH 03/12] enable standard streams invoker can have options and these must be separated in the command incovation. allow changing streams in testing code --- internal/testutil/testutils.go | 20 +++++++++++++---- pkg/command/command.go | 31 +++++++++++++++++++++++++ pkg/summon/run.go | 41 +++++++++++++++++++++++++--------- pkg/summon/run_test.go | 4 ++-- 4 files changed, 79 insertions(+), 17 deletions(-) create mode 100644 pkg/command/command.go diff --git a/internal/testutil/testutils.go b/internal/testutil/testutils.go index 068e87f..0c453d8 100644 --- a/internal/testutil/testutils.go +++ b/internal/testutil/testutils.go @@ -6,6 +6,7 @@ import ( "os" "os/exec" + "github.com/davidovich/summon/pkg/command" "github.com/spf13/afero" ) @@ -24,14 +25,25 @@ func ReplaceFs() func() { } } +type fakeCommand struct { + *exec.Cmd +} + +func (c *fakeCommand) SetStdStreams(stdin io.Reader, stdout, stderr io.Writer) { +} + +func (c *fakeCommand) Run() error { + return c.Cmd.Run() +} + // FakeExecCommand resturns a fake function which calls into testToCall // this is used to mock an exec.Cmd // Adapted from https://npf.io/2015/06/testing-exec-command/ -func FakeExecCommand(testToCall string, stdout, stderr io.Writer) func(string, ...string) *exec.Cmd { - return func(command string, args ...string) *exec.Cmd { - cs := []string{"-test.run=" + testToCall, "--", command} +func FakeExecCommand(testToCall string, stdout, stderr io.Writer) func(string, ...string) command.Commander { + return func(c string, args ...string) command.Commander { + cs := []string{"-test.run=" + testToCall, "--", c} cs = append(cs, args...) - cmd := exec.Command(os.Args[0], cs...) + cmd := &fakeCommand{exec.Command(os.Args[0], cs...)} cmd.Stdout = stdout cmd.Stderr = stderr diff --git a/pkg/command/command.go b/pkg/command/command.go new file mode 100644 index 0000000..6164e15 --- /dev/null +++ b/pkg/command/command.go @@ -0,0 +1,31 @@ +package command + +import ( + "io" + "os/exec" +) + +// Commander describes a subset of a exec.Cmd functionality for testing +type Commander interface { + SetStdStreams(stdin io.Reader, stdout, stderr io.Writer) + Run() error +} + +type cmd struct { + *exec.Cmd +} + +// New creates a concrete commander +func New(c string, args ...string) Commander { + return &cmd{exec.Command(c, args...)} +} + +func (c *cmd) Run() error { + return c.Cmd.Run() +} + +func (c *cmd) SetStdStreams(stdin io.Reader, stdout, stderr io.Writer) { + c.Stdin = stdin + c.Stdout = stdout + c.Stderr = stderr +} diff --git a/pkg/summon/run.go b/pkg/summon/run.go index 97f0f8e..a3d6326 100644 --- a/pkg/summon/run.go +++ b/pkg/summon/run.go @@ -1,29 +1,48 @@ package summon import ( - "os/exec" + "fmt" + "os" + "strings" + + "github.com/davidovich/summon/pkg/command" + "github.com/davidovich/summon/pkg/config" ) -var execCommand = exec.Command +var execCommand = command.New // Run will run go or executable scripts in the context of the data func (s *Summoner) Run(opts ...Option) error { + s.Configure(opts...) + exec, commands, err := s.findExecutor() + if err != nil { + return err + } + + finalCommand := append(commands, s.opts.args...) + + cmd := execCommand(exec, finalCommand...) + cmd.SetStdStreams(os.Stdin, os.Stdout, os.Stderr) + + return cmd.Run() +} + +func (s *Summoner) findExecutor() (string, []string, error) { var executor string - var command string + var commands []string for k, v := range s.config.Executables { if c, ok := v[s.opts.ref]; ok { - executor = k - command = c + exec := strings.Split(k, " ") + executor = exec[0] + commands = append(exec[1:], c) break } } - finalCommand := append([]string{command}, s.opts.args...) - - cmd := execCommand(executor, finalCommand...) - - err := cmd.Run() + if executor == "" { + return "", []string{}, fmt.Errorf("could not find exec reference %s in config %s", s.opts.ref, config.ConfigFile) + } - return err + return executor, commands, nil } diff --git a/pkg/summon/run_test.go b/pkg/summon/run_test.go index f47dc73..27d74b3 100644 --- a/pkg/summon/run_test.go +++ b/pkg/summon/run_test.go @@ -4,11 +4,11 @@ import ( "bytes" "fmt" "os" - "os/exec" "strings" "testing" "github.com/davidovich/summon/internal/testutil" + "github.com/davidovich/summon/pkg/command" "github.com/gobuffalo/packr/v2" "github.com/stretchr/testify/assert" ) @@ -16,7 +16,7 @@ import ( func TestRun(t *testing.T) { stdout := &bytes.Buffer{} execCommand = testutil.FakeExecCommand("TestSummonRunHelper", stdout, nil) - defer func() { execCommand = exec.Command }() + defer func() { execCommand = command.New }() box := packr.New("test run box", "testdata") From 6e7f41193e9c0d0c0b05a3e43c7463b895634acc Mon Sep 17 00:00:00 2001 From: david Date: Tue, 26 Feb 2019 22:30:05 -0500 Subject: [PATCH 04/12] add basic run tests --- cmd/run.go | 1 + pkg/summon/run.go | 6 ++--- pkg/summon/run_test.go | 60 +++++++++++++++++++++++++++++++++++++----- 3 files changed, 58 insertions(+), 9 deletions(-) diff --git a/cmd/run.go b/cmd/run.go index 815027e..94292e7 100644 --- a/cmd/run.go +++ b/cmd/run.go @@ -20,6 +20,7 @@ func newRunCmd(driver summon.Interface) *cobra.Command { Args: cobra.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { runCmd.ref = args[0] + cmd.SilenceUsage = true return runCmd.run() }, } diff --git a/pkg/summon/run.go b/pkg/summon/run.go index a3d6326..997f64d 100644 --- a/pkg/summon/run.go +++ b/pkg/summon/run.go @@ -31,9 +31,9 @@ func (s *Summoner) findExecutor() (string, []string, error) { var executor string var commands []string - for k, v := range s.config.Executables { - if c, ok := v[s.opts.ref]; ok { - exec := strings.Split(k, " ") + for ex, handles := range s.config.Executables { + if c, ok := handles[s.opts.ref]; ok { + exec := strings.Split(ex, " ") executor = exec[0] commands = append(exec[1:], c) break diff --git a/pkg/summon/run_test.go b/pkg/summon/run_test.go index 27d74b3..cbcde90 100644 --- a/pkg/summon/run_test.go +++ b/pkg/summon/run_test.go @@ -14,17 +14,65 @@ import ( ) func TestRun(t *testing.T) { - stdout := &bytes.Buffer{} - execCommand = testutil.FakeExecCommand("TestSummonRunHelper", stdout, nil) defer func() { execCommand = command.New }() box := packr.New("test run box", "testdata") - s := New(box, Ref("hello")) - err := s.Run() + tests := []struct { + name string + helper string + ref string + expect string + wantErr bool + }{ + { + name: "composite-invoker", // python -c + helper: "TestSummonRunHelper", + ref: "hello", + expect: "python -c print(\"hello from python!\")", + wantErr: false, + }, + { + name: "simple-invoker", // bash + helper: "TestSummonRunHelper", + ref: "hello-bash", + expect: "bash hello.sh", + wantErr: false, + }, + { + name: "fail", + ref: "hello", + helper: "TestFailRunHelper", + wantErr: true, + }, + { + name: "fail-no-ref", + ref: "does-not-exist", + helper: "TestSummonRunHelper", + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + stdout := &bytes.Buffer{} + execCommand = testutil.FakeExecCommand(tt.helper, stdout, nil) + + s := New(box, Ref(tt.ref)) + if err := s.Run(); (err != nil) != tt.wantErr { + t.Errorf("summon.Run() error = %v, wantErr %v", err, tt.wantErr) + } + + assert.Equal(t, tt.expect, stdout.String()) + }) + } +} + +func TestFailRunHelper(t *testing.T) { + if os.Getenv("GO_WANT_HELPER_PROCESS") != "1" { + return + } - assert.Nil(t, err) - assert.Equal(t, "python -c print(\"hello from python!\")", stdout.String()) + os.Exit(1) } func TestSummonRunHelper(t *testing.T) { From e3c43bcd2239791d9df301a738eddf8adda324ea Mon Sep 17 00:00:00 2001 From: david Date: Tue, 26 Feb 2019 23:33:33 -0500 Subject: [PATCH 05/12] simplify exec.Cmd testing --- internal/testutil/testutils.go | 28 +++++++++++----------------- pkg/command/command.go | 33 ++++++++++++--------------------- pkg/summon/run.go | 4 +++- 3 files changed, 26 insertions(+), 39 deletions(-) diff --git a/internal/testutil/testutils.go b/internal/testutil/testutils.go index 0c453d8..0f96923 100644 --- a/internal/testutil/testutils.go +++ b/internal/testutil/testutils.go @@ -25,29 +25,23 @@ func ReplaceFs() func() { } } -type fakeCommand struct { - *exec.Cmd -} - -func (c *fakeCommand) SetStdStreams(stdin io.Reader, stdout, stderr io.Writer) { -} - -func (c *fakeCommand) Run() error { - return c.Cmd.Run() -} - // FakeExecCommand resturns a fake function which calls into testToCall // this is used to mock an exec.Cmd // Adapted from https://npf.io/2015/06/testing-exec-command/ -func FakeExecCommand(testToCall string, stdout, stderr io.Writer) func(string, ...string) command.Commander { - return func(c string, args ...string) command.Commander { +func FakeExecCommand(testToCall string, stdout, stderr io.Writer) func(string, ...string) *command.Cmd { + return func(c string, args ...string) *command.Cmd { cs := []string{"-test.run=" + testToCall, "--", c} cs = append(cs, args...) - cmd := &fakeCommand{exec.Command(os.Args[0], cs...)} + cmd := &command.Cmd{ + Cmd: exec.Command(os.Args[0], cs...), + } + cmd.Run = func() error { + cmd.Stdout = stdout + cmd.Stderr = stderr + cmd.Env = []string{"GO_WANT_HELPER_PROCESS=1"} + return cmd.Cmd.Run() + } - cmd.Stdout = stdout - cmd.Stderr = stderr - cmd.Env = []string{"GO_WANT_HELPER_PROCESS=1"} return cmd } } diff --git a/pkg/command/command.go b/pkg/command/command.go index 6164e15..a60f53c 100644 --- a/pkg/command/command.go +++ b/pkg/command/command.go @@ -1,31 +1,22 @@ package command import ( - "io" "os/exec" ) -// Commander describes a subset of a exec.Cmd functionality for testing -type Commander interface { - SetStdStreams(stdin io.Reader, stdout, stderr io.Writer) - Run() error -} - -type cmd struct { +// Cmd is an exec.Cmd with a configurable Run function +type Cmd struct { *exec.Cmd + Run func() error } -// New creates a concrete commander -func New(c string, args ...string) Commander { - return &cmd{exec.Command(c, args...)} -} - -func (c *cmd) Run() error { - return c.Cmd.Run() -} - -func (c *cmd) SetStdStreams(stdin io.Reader, stdout, stderr io.Writer) { - c.Stdin = stdin - c.Stdout = stdout - c.Stderr = stderr +// New creates a Cmd with a real exec.Cmd Run function +func New(c string, args ...string) *Cmd { + cmd := &Cmd{ + Cmd: exec.Command(c, args...), + } + cmd.Run = func() error { + return cmd.Cmd.Run() + } + return cmd } diff --git a/pkg/summon/run.go b/pkg/summon/run.go index 997f64d..c7120b6 100644 --- a/pkg/summon/run.go +++ b/pkg/summon/run.go @@ -22,7 +22,9 @@ func (s *Summoner) Run(opts ...Option) error { finalCommand := append(commands, s.opts.args...) cmd := execCommand(exec, finalCommand...) - cmd.SetStdStreams(os.Stdin, os.Stdout, os.Stderr) + cmd.Stdin = os.Stdin + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr return cmd.Run() } From 46c85a6706b3a444a8694a53f66ca692fbc162b5 Mon Sep 17 00:00:00 2001 From: david Date: Tue, 26 Feb 2019 23:35:08 -0500 Subject: [PATCH 06/12] fix spelling --- pkg/summon/options_test.go | 2 +- pkg/summon/testdata/summon.config.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/summon/options_test.go b/pkg/summon/options_test.go index 068b825..24efbb1 100644 --- a/pkg/summon/options_test.go +++ b/pkg/summon/options_test.go @@ -12,5 +12,5 @@ func TestBoxedConfig(t *testing.T) { s := New(box) - assert.Equal(t, "overriden_dir", s.opts.destination) + assert.Equal(t, "overridden_dir", s.opts.destination) } diff --git a/pkg/summon/testdata/summon.config.yaml b/pkg/summon/testdata/summon.config.yaml index a166c44..c292424 100644 --- a/pkg/summon/testdata/summon.config.yaml +++ b/pkg/summon/testdata/summon.config.yaml @@ -1,6 +1,6 @@ version: 1 aliases: {} -outputdir: "overriden_dir" +outputdir: "overridden_dir" exec: bash: hello-bash: hello.sh From 7e5158618ca36421b3194db993ad938fe5f6546d Mon Sep 17 00:00:00 2001 From: david Date: Sat, 2 Mar 2019 15:51:23 -0500 Subject: [PATCH 07/12] testing: added possibility to record more complex info from test helper serialize a call sequence to be hydrated in the tests --- internal/testutil/testutils.go | 57 ++++++++++++++++++++++++++++- internal/testutil/testutils_test.go | 49 +++++++++++++++++++++++++ pkg/summon/run_test.go | 19 ++++++++-- 3 files changed, 119 insertions(+), 6 deletions(-) create mode 100644 internal/testutil/testutils_test.go diff --git a/internal/testutil/testutils.go b/internal/testutil/testutils.go index 0f96923..c1f3138 100644 --- a/internal/testutil/testutils.go +++ b/internal/testutil/testutils.go @@ -1,8 +1,10 @@ package testutil import ( + "encoding/json" "fmt" "io" + "io/ioutil" "os" "os/exec" @@ -25,27 +27,78 @@ func ReplaceFs() func() { } } +//Call is a recording of a fake call +type Call struct { + Args string + Env []string +} + +// Calls is the array of calls +type Calls struct { + Calls []Call +} + // FakeExecCommand resturns a fake function which calls into testToCall // this is used to mock an exec.Cmd // Adapted from https://npf.io/2015/06/testing-exec-command/ func FakeExecCommand(testToCall string, stdout, stderr io.Writer) func(string, ...string) *command.Cmd { + calls := 0 return func(c string, args ...string) *command.Cmd { cs := []string{"-test.run=" + testToCall, "--", c} cs = append(cs, args...) cmd := &command.Cmd{ Cmd: exec.Command(os.Args[0], cs...), } + if calls == 0 { + startCall(stdout) + } cmd.Run = func() error { + if calls > 0 { + willAppendCall(stdout) + } cmd.Stdout = stdout cmd.Stderr = stderr - cmd.Env = []string{"GO_WANT_HELPER_PROCESS=1"} - return cmd.Cmd.Run() + cmd.Env = append(cmd.Env, "GO_WANT_HELPER_PROCESS=1") + err := cmd.Cmd.Run() + calls++ + return err } return cmd } } +func startCall(out io.Writer) { + out.Write([]byte("{\"Calls\":[")) +} + +func willAppendCall(out io.Writer) { + out.Write([]byte(",")) +} + +// WriteCall marshals the executable call with env +func WriteCall(c Call, w io.Writer) error { + b, err := json.Marshal(c) + if err != nil { + return err + } + w.Write(b) + return nil +} + +// GetCalls ends and returns a call sequence +func GetCalls(out io.Reader) (*Calls, error) { + c := &Calls{} + buf, _ := ioutil.ReadAll(out) + if len(buf) == 0 { + return c, nil + } + buf = append(buf, []byte("]}")...) + err := json.Unmarshal(buf, c) + + return c, err +} + // CleanHelperArgs removes the helper process arguments func CleanHelperArgs(helperArgs []string) []string { args := os.Args diff --git a/internal/testutil/testutils_test.go b/internal/testutil/testutils_test.go new file mode 100644 index 0000000..02be657 --- /dev/null +++ b/internal/testutil/testutils_test.go @@ -0,0 +1,49 @@ +package testutil + +import ( + "bytes" + "encoding/json" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestUnMarshallCall(t *testing.T) { + out := &bytes.Buffer{} + + call1 := Call{ + Args: "a b c", + Env: []string{"a=b"}, + } + call2 := Call{ + Args: "a b c", + Env: []string{"a=b"}, + } + + startCall(out) + + WriteCall(call1, out) + willAppendCall(out) + WriteCall(call2, out) + + c, err := GetCalls(out) + + assert.Nil(t, err) + assert.Equal(t, 2, len(c.Calls)) + assert.Contains(t, c.Calls, call1) + assert.Contains(t, c.Calls, call2) +} + +func TestMarshallCalls(t *testing.T) { + c := Calls{Calls: []Call{ + Call{ + Args: "a b c", + Env: []string{"a=b"}, + }, + }} + + b, err := json.Marshal(c) + + assert.Nil(t, err) + assert.Equal(t, "{\"Calls\":[{\"Args\":\"a b c\",\"Env\":[\"a=b\"]}]}", string(b)) +} diff --git a/pkg/summon/run_test.go b/pkg/summon/run_test.go index cbcde90..a13ccea 100644 --- a/pkg/summon/run_test.go +++ b/pkg/summon/run_test.go @@ -2,7 +2,6 @@ package summon import ( "bytes" - "fmt" "os" "strings" "testing" @@ -62,7 +61,15 @@ func TestRun(t *testing.T) { t.Errorf("summon.Run() error = %v, wantErr %v", err, tt.wantErr) } - assert.Equal(t, tt.expect, stdout.String()) + c, err := testutil.GetCalls(stdout) + assert.Nil(t, err) + + if tt.wantErr { + assert.Len(t, c.Calls, 0) + + } else { + assert.Equal(t, tt.expect, c.Calls[0].Args) + } }) } } @@ -81,6 +88,10 @@ func TestSummonRunHelper(t *testing.T) { } defer os.Exit(0) - args := testutil.CleanHelperArgs(os.Args) - fmt.Fprintf(os.Stdout, strings.Join(args, " ")) + call := testutil.Call{ + Args: strings.Join(testutil.CleanHelperArgs(os.Args), " "), + Env: os.Environ(), + } + + testutil.WriteCall(call, os.Stdout) } From 5ef89c480f76fb3e20ddc3b2f65a307223a60dbd Mon Sep 17 00:00:00 2001 From: david Date: Sun, 3 Mar 2019 15:23:52 -0500 Subject: [PATCH 08/12] output correct exit code (available since go 1.12) --- .circleci/config.yml | 2 +- main.go | 6 +++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 63e53d5..1c81d20 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -6,7 +6,7 @@ version: 2 jobs: build: docker: - - image: circleci/golang:1.11.5 + - image: circleci/golang:1.12 environment: GO111MODULE=on steps: diff --git a/main.go b/main.go index 89db9bf..6a9824b 100755 --- a/main.go +++ b/main.go @@ -1,6 +1,8 @@ package summon import ( + "os/exec" + "github.com/gobuffalo/packr/v2" "github.com/davidovich/summon/cmd" @@ -11,7 +13,9 @@ func Main(args []string, box *packr.Box) int { err := cmd.Execute(box) if err != nil { - return 1 + if exitError, ok := err.(*exec.ExitError); ok { + return exitError.ExitCode() + } } return 0 From cc3f0e7e0a58dd76b41e1ecce5b1d6d151cacfea Mon Sep 17 00:00:00 2001 From: david Date: Sun, 3 Mar 2019 15:31:00 -0500 Subject: [PATCH 09/12] always seed default outputDir --- pkg/summon/summon_test.go | 8 ++++---- pkg/summon/summoner.go | 4 ++++ 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/pkg/summon/summon_test.go b/pkg/summon/summon_test.go index 2dff3a3..4671a54 100644 --- a/pkg/summon/summon_test.go +++ b/pkg/summon/summon_test.go @@ -26,7 +26,7 @@ func TestErrorOnMissingFiles(t *testing.T) { func TestMultifileInstanciation(t *testing.T) { defer testutil.ReplaceFs()() - box := packr.New("test box", "") + box := packr.New("test box multifile", "") box.AddString("text.txt", "this is a text") box.AddString("another.txt", "another text") @@ -35,12 +35,12 @@ func TestMultifileInstanciation(t *testing.T) { path, err := s.Summon() assert.Nil(t, err) - assert.Equal(t, "", path) + assert.Equal(t, ".summoned", path) - _, err = appFs.Stat("text.txt") + _, err = appFs.Stat(".summoned/text.txt") assert.Nil(t, err) - _, err = appFs.Stat("another.txt") + _, err = appFs.Stat(".summoned/another.txt") assert.Nil(t, err) } diff --git a/pkg/summon/summoner.go b/pkg/summon/summoner.go index 5224a62..6f5adf3 100644 --- a/pkg/summon/summoner.go +++ b/pkg/summon/summoner.go @@ -25,6 +25,10 @@ func New(box *packr.Box, opts ...Option) *Summoner { // Configure is used to extract options to the object. func (b *Summoner) Configure(opts ...Option) { + if b.opts.destination == "" { + b.opts.destination = config.DefaultOutputDir + } + // try to find a config file in the box config, err := b.box.Find(config.ConfigFile) if err == nil { From 742c0b39fa569a162e93efad4f44d9ac5392741f Mon Sep 17 00:00:00 2001 From: david Date: Sun, 3 Mar 2019 15:52:34 -0500 Subject: [PATCH 10/12] allow running go gettable executables through gobin --- README.md | 21 +++++--- cmd/run.go | 21 ++++++-- go.mod | 1 + pkg/summon/run.go | 71 ++++++++++++++++++++++---- pkg/summon/testdata/summon.config.yaml | 6 +-- 5 files changed, 97 insertions(+), 23 deletions(-) diff --git a/README.md b/README.md index 687a8ae..3fb3227 100755 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ not every feature is implemented yet. Why not use git directly? While you could use git directly to bring an asset directory with a simple git clone, the result does not have executable properties. -while in summon you leverage go execution to bootstrap in one phase. So your data can do: +In summon you leverage go execution to bootstrap in one phase. So your data can do: ``` go run github.com/davidovich/summon-example-assets/summon --help @@ -62,13 +62,12 @@ The `assets/summon.config.yaml` contains a configuration file to customize summo * aliases * default output-dir - * gobin flags * executables ```yaml version: 1 - +outputdir: .summoned # exec section declares invokables with their handle # a same handle name cannot be in two invokers at the same time exec: @@ -78,11 +77,21 @@ exec: hello: echo hello # ^ handle to script (must be unique) - go: # go gettable executables - gobin: github.com/myitcv/gobin + gobin: # go gettable executables + gobin: github.com/myitcv/gobin@v0.0.8 gohack: github.com/rogppepe/gohack + + python -c: + hello: print("hello from python!") +``` + +You can then invoke the executable like so: + +``` +summon run gohack ... ``` +This will install gohack and forward the arguments that you provide. Build ----- @@ -126,7 +135,7 @@ include $(shell summon version.mk) By default, summon will put summoned scripts at the `.summoned/` directory at root of the current directory. -### Running a go binary (soon) +### Running a go binary `summon run [executable]` allows to run executables declared in the config file diff --git a/cmd/run.go b/cmd/run.go index 94292e7..17416fa 100644 --- a/cmd/run.go +++ b/cmd/run.go @@ -1,6 +1,8 @@ package cmd import ( + "os" + "github.com/davidovich/summon/pkg/summon" "github.com/spf13/cobra" ) @@ -8,6 +10,7 @@ import ( type runCmdOpts struct { driver summon.Interface ref string + args []string } func newRunCmd(driver summon.Interface) *cobra.Command { @@ -17,10 +20,19 @@ func newRunCmd(driver summon.Interface) *cobra.Command { rcmd := &cobra.Command{ Use: "run", Short: "launch executable from summonables", - Args: cobra.ExactArgs(1), + FParseErrWhitelist: cobra.FParseErrWhitelist{ + UnknownFlags: true, + }, RunE: func(cmd *cobra.Command, args []string) error { - runCmd.ref = args[0] cmd.SilenceUsage = true + + runCmd.ref = args[0] + // pass all Args down to the referenced executable + // this is due to a limitation in spf13/cobra which eats + // all unknown args or flags making it hard to wrap other commands + // we are lucky, we know the structure, just pass all args. + // see https://github.com/spf13/pflag/pull/160 + runCmd.args = os.Args[3:] // 3 is [summon, run, handle] return runCmd.run() }, } @@ -29,7 +41,10 @@ func newRunCmd(driver summon.Interface) *cobra.Command { } func (r *runCmdOpts) run() error { - r.driver.Configure(summon.Ref(r.ref)) + r.driver.Configure( + summon.Ref(r.ref), + summon.Args(r.args...), + ) return r.driver.Run() } diff --git a/go.mod b/go.mod index 191c5cb..b992ed4 100644 --- a/go.mod +++ b/go.mod @@ -7,6 +7,7 @@ require ( github.com/gobuffalo/meta v0.0.0-20190207205153-50a99e08b8cf // indirect github.com/gobuffalo/packr/v2 v2.0.1 github.com/lithammer/dedent v1.1.0 + github.com/pkg/errors v0.8.1 github.com/rogpeppe/go-internal v1.2.1 // indirect github.com/spf13/afero v1.2.1 github.com/spf13/cobra v0.0.3 diff --git a/pkg/summon/run.go b/pkg/summon/run.go index c7120b6..5b556e0 100644 --- a/pkg/summon/run.go +++ b/pkg/summon/run.go @@ -1,27 +1,46 @@ package summon import ( + "bytes" "fmt" "os" + "path/filepath" "strings" "github.com/davidovich/summon/pkg/command" "github.com/davidovich/summon/pkg/config" + "github.com/pkg/errors" ) var execCommand = command.New +type execUnit struct { + invoker string + invOpts []string + target string +} + // Run will run go or executable scripts in the context of the data func (s *Summoner) Run(opts ...Option) error { s.Configure(opts...) - exec, commands, err := s.findExecutor() + + eu, err := s.findExecutor() if err != nil { return err } - finalCommand := append(commands, s.opts.args...) + eu, err = s.resolve(eu) + if err != nil { + return errors.Wrapf(err, "resolving %s", eu.invoker) + } - cmd := execCommand(exec, finalCommand...) + args := eu.invOpts + if eu.target != "" { + args = append(args, eu.target) + } + args = append(args, s.opts.args...) + + cmd := execCommand(eu.invoker, args...) cmd.Stdin = os.Stdin cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr @@ -29,22 +48,52 @@ func (s *Summoner) Run(opts ...Option) error { return cmd.Run() } -func (s *Summoner) findExecutor() (string, []string, error) { - var executor string - var commands []string +func (s *Summoner) findExecutor() (execUnit, error) { + eu := execUnit{} for ex, handles := range s.config.Executables { if c, ok := handles[s.opts.ref]; ok { exec := strings.Split(ex, " ") - executor = exec[0] - commands = append(exec[1:], c) + eu.invoker = exec[0] + eu.invOpts = exec[1:] + eu.target = c break } } - if executor == "" { - return "", []string{}, fmt.Errorf("could not find exec reference %s in config %s", s.opts.ref, config.ConfigFile) + if eu.invoker == "" { + return eu, fmt.Errorf("could not find exec handle reference %s in config %s", s.opts.ref, config.ConfigFile) } - return executor, commands, nil + return eu, nil +} + +func (s *Summoner) resolve(execu execUnit) (execUnit, error) { + if strings.HasPrefix("gobin", execu.invoker) { + return s.prepareGoBinExecutable(execu) + } + return execu, nil +} + +func (s *Summoner) prepareGoBinExecutable(execu execUnit) (execUnit, error) { + // install in OutputDir + target := strings.Split(execu.target, "@")[0] + targetDir := filepath.Join(s.opts.destination, filepath.Dir(target)) + cmd := execCommand(execu.invoker, execu.target) + cmd.Env = append(os.Environ(), "GOBIN="+targetDir) + buf := &bytes.Buffer{} + cmd.Stdout = buf + cmd.Stderr = buf + //fmt.Printf("executing: %s\n", cmd.Args) + err := cmd.Run() + + if err != nil { + err = errors.Wrapf(err, "executing: %s: %s", cmd.Args, buf) + } + + execu.invoker = filepath.Join(targetDir, filepath.Base(target)) + execu.invOpts = []string{} + execu.target = "" + + return execu, err } diff --git a/pkg/summon/testdata/summon.config.yaml b/pkg/summon/testdata/summon.config.yaml index c292424..07e4685 100644 --- a/pkg/summon/testdata/summon.config.yaml +++ b/pkg/summon/testdata/summon.config.yaml @@ -4,8 +4,8 @@ outputdir: "overridden_dir" exec: bash: hello-bash: hello.sh - go: - gobin: github.com/myitcv/gobin - gohack: github.com/rogppepe/gohack + gobin: + gobin: github.com/myitcv/gobin@v0.0.8 + gohack: github.com/rogpeppe/gohack python -c: hello: print("hello from python!") From 8fa5b1e14c671d508a1395a35fafb465d4c96e75 Mon Sep 17 00:00:00 2001 From: david Date: Fri, 8 Mar 2019 17:42:26 -0500 Subject: [PATCH 11/12] add tests for gobin invoker Allow version to be specified correctly populate cmd.Env for gobin subprocess --- pkg/summon/run.go | 18 +++++++------ pkg/summon/run_test.go | 59 +++++++++++++++++++++++++++++++++++++++++- 2 files changed, 68 insertions(+), 9 deletions(-) diff --git a/pkg/summon/run.go b/pkg/summon/run.go index 5b556e0..6e8a14f 100644 --- a/pkg/summon/run.go +++ b/pkg/summon/run.go @@ -29,7 +29,7 @@ func (s *Summoner) Run(opts ...Option) error { return err } - eu, err = s.resolve(eu) + eu, err = s.resolve(eu, os.Environ()) if err != nil { return errors.Wrapf(err, "resolving %s", eu.invoker) } @@ -68,23 +68,25 @@ func (s *Summoner) findExecutor() (execUnit, error) { return eu, nil } -func (s *Summoner) resolve(execu execUnit) (execUnit, error) { - if strings.HasPrefix("gobin", execu.invoker) { - return s.prepareGoBinExecutable(execu) +// resolve prepares special invokers for execution +// presently supported: gobin +func (s *Summoner) resolve(execu execUnit, environ []string) (execUnit, error) { + if strings.HasPrefix(execu.invoker, "gobin") { + return s.prepareGoBinExecutable(execu, environ) } return execu, nil } -func (s *Summoner) prepareGoBinExecutable(execu execUnit) (execUnit, error) { +func (s *Summoner) prepareGoBinExecutable(execu execUnit, environ []string) (execUnit, error) { // install in OutputDir + dest := s.opts.destination target := strings.Split(execu.target, "@")[0] - targetDir := filepath.Join(s.opts.destination, filepath.Dir(target)) + targetDir := filepath.Join(dest, filepath.Dir(target)) cmd := execCommand(execu.invoker, execu.target) - cmd.Env = append(os.Environ(), "GOBIN="+targetDir) + cmd.Env = append(environ, "GOBIN="+targetDir) buf := &bytes.Buffer{} cmd.Stdout = buf cmd.Stderr = buf - //fmt.Printf("executing: %s\n", cmd.Args) err := cmd.Run() if err != nil { diff --git a/pkg/summon/run_test.go b/pkg/summon/run_test.go index a13ccea..a3a15e5 100644 --- a/pkg/summon/run_test.go +++ b/pkg/summon/run_test.go @@ -66,7 +66,6 @@ func TestRun(t *testing.T) { if tt.wantErr { assert.Len(t, c.Calls, 0) - } else { assert.Equal(t, tt.expect, c.Calls[0].Args) } @@ -74,6 +73,64 @@ func TestRun(t *testing.T) { } } +func TestResolveExecUnit(t *testing.T) { + + testCases := []struct { + desc string + execu execUnit + expected execUnit + wantsCalls bool + expectedArg string + expectedEnv string + }{ + { + desc: "gobin", + execu: execUnit{ + invoker: "gobin", + target: "github.com/myitcv/gobin@v0.0.8", + }, + expected: execUnit{ + invoker: ".summoned/github.com/myitcv/gobin", + target: "", + invOpts: []string{}, + }, + wantsCalls: true, + expectedArg: "gobin github.com/myitcv/gobin", + expectedEnv: "GOBIN=.summoned/github.com/myitcv", + }, + { + desc: "non-gobin", + execu: execUnit{ + invoker: "python", + target: "script.py", + }, + expected: execUnit{ + invoker: "python", + target: "script.py", + }}, + } + for _, tC := range testCases { + t.Run(tC.desc, func(t *testing.T) { + stdout := &bytes.Buffer{} + execCommand = testutil.FakeExecCommand("TestSummonRunHelper", stdout, nil) + + s := New(packr.New("t", "testdata"), Dest(".summoned")) + eu, err := s.resolve(tC.execu, []string{}) + + assert.Nil(t, err) + assert.Equal(t, tC.expected, eu) + + if tC.wantsCalls { + c, err := testutil.GetCalls(stdout) + assert.Nil(t, err) + assert.Len(t, c.Calls, 1) + assert.Contains(t, c.Calls[0].Env, tC.expectedEnv) + assert.Contains(t, c.Calls[0].Args, tC.expectedArg) + } + }) + } +} + func TestFailRunHelper(t *testing.T) { if os.Getenv("GO_WANT_HELPER_PROCESS") != "1" { return From 535e9db182c508f3e536fadfda4776c52ea47783 Mon Sep 17 00:00:00 2001 From: david Date: Sat, 9 Mar 2019 23:51:08 -0500 Subject: [PATCH 12/12] test: make box unique --- pkg/summon/summon_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/summon/summon_test.go b/pkg/summon/summon_test.go index 4671a54..96d5779 100644 --- a/pkg/summon/summon_test.go +++ b/pkg/summon/summon_test.go @@ -49,7 +49,7 @@ func TestOneFileInstanciation(t *testing.T) { a := assert.New(t) - box := packr.New("t", "") + box := packr.New("t1", "") box.AddString("text.txt", "this is a text") // create a summoner to summon text.txt at