From 7daa3285d6c92ee78f895a6e42b0cab107cc89d4 Mon Sep 17 00:00:00 2001 From: Xiao Cui Date: Tue, 16 Jul 2024 13:22:38 -0400 Subject: [PATCH] Load extension binary as subcommand --- pkg/commands/extensions/extension.go | 33 +++++ pkg/commands/extensions/extension_test.go | 168 ++++++++++++++++++++++ 2 files changed, 201 insertions(+) diff --git a/pkg/commands/extensions/extension.go b/pkg/commands/extensions/extension.go index 28f74e3..316f116 100644 --- a/pkg/commands/extensions/extension.go +++ b/pkg/commands/extensions/extension.go @@ -57,6 +57,7 @@ func NewCmdExtension(iostream *iostreams.IOStreams) *cobra.Command { cmd.AddCommand(newExtensionListCmd(iostream)) cmd.AddCommand(newExtensionRemoveCmd(iostream)) cmd.AddCommand(newExtensionUpdateCmd(iostream)) + cmd.AddCommand(newExtensionRunCmd()) // Add this new subcommand return cmd } @@ -163,3 +164,35 @@ func newExtensionUpdateCmd(iostream *iostreams.IOStreams) *cobra.Command { }, } } + +func newExtensionRunCmd() *cobra.Command { + return &cobra.Command{ + Use: "run [args...]", + Short: "Run a mycli extension", + Args: cobra.MinimumNArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + if len(args) == 0 { + return fmt.Errorf("extension name is required") + } + extName := args[0] + extArgs := args[1:] + return runExtension(extName, extArgs) + }, + } +} + +func runExtension(extName string, args []string) error { + extDir := getExtensionDir() + extPath := filepath.Join(extDir, "mycli-"+extName, "mycli-"+extName) + + if _, err := os.Stat(extPath); os.IsNotExist(err) { + return fmt.Errorf("extension '%s' not found", extName) + } + + cmd := exec.Command(extPath, args...) + cmd.Stdin = os.Stdin + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + + return cmd.Run() +} diff --git a/pkg/commands/extensions/extension_test.go b/pkg/commands/extensions/extension_test.go index cab4ee9..6f71bf3 100644 --- a/pkg/commands/extensions/extension_test.go +++ b/pkg/commands/extensions/extension_test.go @@ -1,6 +1,7 @@ package extensions import ( + "bytes" "net/http" "net/http/httptest" "os" @@ -9,6 +10,7 @@ import ( "testing" "github.com/XiaoConstantine/mycli/pkg/iostreams" + "github.com/spf13/cobra" "github.com/stretchr/testify/assert" ) @@ -238,6 +240,172 @@ func TestExtensionInstallCommand(t *testing.T) { } } +func TestRunExtension(t *testing.T) { + // Create a temporary directory to simulate the extensions directory + tempDir, err := os.MkdirTemp("", "mycli-test-extensions") + if err != nil { + t.Fatalf("Failed to create temp directory: %v", err) + } + defer os.RemoveAll(tempDir) + + // Mock the getExtensionDir function + oldGetExtensionDir := getExtensionDir + getExtensionDir = func() string { return tempDir } + defer func() { getExtensionDir = oldGetExtensionDir }() + + // Create a mock extension + mockExtName := "mock-extension" + mockExtDir := filepath.Join(tempDir, "mycli-"+mockExtName) + mockExtPath := filepath.Join(mockExtDir, "mycli-"+mockExtName) + err = os.MkdirAll(mockExtDir, 0755) + assert.NoError(t, err) + + // Create a mock executable + mockExtContent := []byte("#!/bin/sh\necho \"Mock extension executed with args: $@\"") + err = os.WriteFile(mockExtPath, mockExtContent, 0755) + assert.NoError(t, err) + + // Test cases + testCases := []struct { + name string + extName string + args []string + expectedError bool + expectedOutput string + }{ + { + name: "Existing extension", + extName: mockExtName, + args: []string{"arg1", "arg2"}, + expectedError: false, + expectedOutput: "Mock extension executed with args: arg1 arg2\n", + }, + { + name: "Non-existent extension", + extName: "non-existent", + args: []string{}, + expectedError: true, + expectedOutput: "", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + // Capture stdout + oldStdout := os.Stdout + r, w, _ := os.Pipe() + os.Stdout = w + + err := runExtension(tc.extName, tc.args) + + // Restore stdout + w.Close() + os.Stdout = oldStdout + + // Read captured output + var buf bytes.Buffer + _, _ = buf.ReadFrom(r) + output := buf.String() + + if tc.expectedError { + assert.Error(t, err) + } else { + assert.NoError(t, err) + assert.Equal(t, tc.expectedOutput, output) + } + }) + } +} + +func TestNewExtensionRunCmd(t *testing.T) { + // Create a temporary directory to simulate the extensions directory + tempDir, err := os.MkdirTemp("", "mycli-test-extensions") + if err != nil { + t.Fatalf("Failed to create temp directory: %v", err) + } + defer os.RemoveAll(tempDir) + + // Mock the getExtensionDir function + oldGetExtensionDir := getExtensionDir + getExtensionDir = func() string { return tempDir } + defer func() { getExtensionDir = oldGetExtensionDir }() + + // Create a mock extension + mockExtName := "mock-extension" + mockExtDir := filepath.Join(tempDir, "mycli-"+mockExtName) + mockExtPath := filepath.Join(mockExtDir, "mycli-"+mockExtName) + err = os.MkdirAll(mockExtDir, 0755) + assert.NoError(t, err) + + // Create a mock executable + mockExtContent := []byte("#!/bin/sh\necho \"Mock extension executed with args: $@\"") + err = os.WriteFile(mockExtPath, mockExtContent, 0755) + assert.NoError(t, err) + + // Create the run command + runCmd := newExtensionRunCmd() + + testCases := []struct { + name string + args []string + expectedError bool + expectedOutput string + expectedErrMsg string + }{ + { + name: "Run existing extension", + args: []string{mockExtName, "arg1", "arg2"}, + expectedError: false, + expectedOutput: "Mock extension executed with args: arg1 arg2\n", + }, + { + name: "Run non-existent extension", + args: []string{"non-existent"}, + expectedError: true, + expectedErrMsg: "extension 'non-existent' not found", + }, + { + name: "No arguments", + args: []string{}, + expectedError: true, + expectedErrMsg: "extension name is required", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + // Capture stdout + oldStdout := os.Stdout + r, w, _ := os.Pipe() + os.Stdout = w + + // Execute the command + cmd := &cobra.Command{} + cmd.SetArgs(tc.args) + err := runCmd.RunE(cmd, tc.args) + + // Restore stdout + w.Close() + os.Stdout = oldStdout + + // Read captured output + var buf bytes.Buffer + _, _ = buf.ReadFrom(r) + output := buf.String() + + if tc.expectedError { + assert.Error(t, err) + if tc.expectedErrMsg != "" { + assert.Contains(t, err.Error(), tc.expectedErrMsg) + } + } else { + assert.NoError(t, err) + assert.Equal(t, tc.expectedOutput, output) + } + }) + } +} + // TestHelperProcess isn't a real test. It's used to mock exec.Command in the main test. func TestHelperProcess(t *testing.T) { if os.Getenv("GO_WANT_HELPER_PROCESS") != "1" {