diff --git a/.vscode/settings.json b/.vscode/settings.json index d37c537ed..d186bb770 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -13,7 +13,8 @@ "--proto_path=/usr/local/include/protoc" ] }, - "go.buildTags": "test_with_docker,test_with_txtar" + "go.buildTags": "test_with_docker,test_with_txtar", + "makefile.configureOnOpen": false // Uncomment if you want to work on files in ./web. // "go.buildTags": "js,wasm", // Uncomment if you want to check compilation errors on Windows. diff --git a/cmd/gqltool/main.go b/cmd/gqltool/main.go index bcd1115ce..772fd7936 100644 --- a/cmd/gqltool/main.go +++ b/cmd/gqltool/main.go @@ -11,7 +11,6 @@ import ( "github.com/stateful/runme/v3/internal/client/graphql" ) -// var tokenDir = flag.String("token-dir", cmd.GetUserConfigHome(), "The directory with tokens") var apiURL = flag.String("api-url", "http://localhost:4000", "The API base address") func init() { diff --git a/internal/cmd/beta/beta_cmd.go b/internal/cmd/beta/beta_cmd.go index 5412819a4..65620a890 100644 --- a/internal/cmd/beta/beta_cmd.go +++ b/internal/cmd/beta/beta_cmd.go @@ -59,6 +59,7 @@ All commands use the runme.yaml configuration file.`, return nil }) + // Print the error to stderr but don't return it because error modes // are neither fully baked yet nor ready for users to consume. if err != nil { diff --git a/internal/cmd/beta/run_cmd.go b/internal/cmd/beta/run_cmd.go index c5fd80f5f..4789faafe 100644 --- a/internal/cmd/beta/run_cmd.go +++ b/internal/cmd/beta/run_cmd.go @@ -2,7 +2,10 @@ package beta import ( "context" + "io" + "os" + "github.com/creack/pty" "github.com/pkg/errors" "github.com/spf13/cobra" "go.uber.org/zap" @@ -10,6 +13,7 @@ import ( "github.com/stateful/runme/v3/internal/command" "github.com/stateful/runme/v3/internal/config/autoconfig" rcontext "github.com/stateful/runme/v3/internal/runner/context" + "github.com/stateful/runme/v3/internal/runnerv2client" "github.com/stateful/runme/v3/internal/session" runnerv2 "github.com/stateful/runme/v3/pkg/api/gen/proto/go/runme/runner/v2" "github.com/stateful/runme/v3/pkg/document" @@ -17,6 +21,8 @@ import ( ) func runCmd(*commonFlags) *cobra.Command { + var remote bool + cmd := cobra.Command{ Use: "run [command1 command2 ...]", Aliases: []string{"exec"}, @@ -36,6 +42,7 @@ Run all blocks from the "setup" and "teardown" tags: RunE: func(cmd *cobra.Command, args []string) error { return autoconfig.InvokeForCommand( func( + clientFactory autoconfig.ClientFactory, cmdFactory command.Factory, filters []project.Filter, logger *zap.Logger, @@ -67,21 +74,57 @@ Run all blocks from the "setup" and "teardown" tags: return errors.WithStack(err) } - session, err := session.New( - session.WithOwl(false), - session.WithProject(proj), - session.WithSeedEnv(nil), - ) - if err != nil { - return err - } - options := getCommandOptions(cmd, session) + ctx := cmd.Context() + + if remote { + client, err := clientFactory() + if err != nil { + return err + } + + sessionResp, err := client.CreateSession( + ctx, + &runnerv2.CreateSessionRequest{ + Project: &runnerv2.Project{ + Root: proj.Root(), + EnvLoadOrder: proj.EnvFilesReadOrder(), + }, + }, + ) + if err != nil { + return errors.WithMessage(err, "failed to create session") + } - for _, t := range tasks { - err := runCodeBlock(cmd.Context(), t.CodeBlock, cmdFactory, options) + for _, t := range tasks { + err := runCodeBlockWithClient( + ctx, + cmd, + client, + t.CodeBlock, + sessionResp.GetSession().GetId(), + ) + if err != nil { + return err + } + } + } else { + session, err := session.New( + session.WithOwl(false), + session.WithProject(proj), + session.WithSeedEnv(nil), + ) if err != nil { return err } + + options := createCommandOptions(cmd, session) + + for _, t := range tasks { + err := runCodeBlock(ctx, t.CodeBlock, cmdFactory, options) + if err != nil { + return err + } + } } return nil @@ -90,10 +133,12 @@ Run all blocks from the "setup" and "teardown" tags: }, } + cmd.Flags().BoolVarP(&remote, "remote", "r", false, "Run commands on a remote server.") + return &cmd } -func getCommandOptions( +func createCommandOptions( cmd *cobra.Command, sess *session.Session, ) command.CommandOptions { @@ -105,12 +150,7 @@ func getCommandOptions( } } -func runCodeBlock( - ctx context.Context, - block *document.CodeBlock, - factory command.Factory, - options command.CommandOptions, -) error { +func createProgramConfigFromCodeBlock(block *document.CodeBlock, opts ...command.ConfigBuilderOption) (*command.ProgramConfig, error) { // TODO(adamb): [command.Config] is generated exclusively from the [document.CodeBlock]. // As we introduce some document- and block-related configs in runme.yaml (root but also nested), // this [Command.Config] should be further extended. @@ -121,7 +161,16 @@ func runCodeBlock( // the last element of the returned config chain. Finally, [command.Config] should be updated. // This algorithm should be likely encapsulated in the [internal/config] and [internal/command] // packages. - cfg, err := command.NewProgramConfigFromCodeBlock(block) + return command.NewProgramConfigFromCodeBlock(block, opts...) +} + +func runCodeBlock( + ctx context.Context, + block *document.CodeBlock, + factory command.Factory, + options command.CommandOptions, +) error { + cfg, err := createProgramConfigFromCodeBlock(block) if err != nil { return err } @@ -138,9 +187,45 @@ func runCodeBlock( if err != nil { return err } - err = cmd.Start(ctx) - if err != nil { + if err := cmd.Start(ctx); err != nil { return err } return cmd.Wait(ctx) } + +func runCodeBlockWithClient( + ctx context.Context, + cobraCommand *cobra.Command, + client *runnerv2client.Client, + block *document.CodeBlock, + sessionID string, +) error { + cfg, err := createProgramConfigFromCodeBlock(block, command.WithInteractiveLegacy()) + if err != nil { + return err + } + + opts := runnerv2client.ExecuteProgramOptions{ + SessionID: sessionID, + Stdin: io.NopCloser(cobraCommand.InOrStdin()), + Stdout: cobraCommand.OutOrStdout(), + Stderr: cobraCommand.ErrOrStderr(), + StoreStdoutInEnv: true, + } + + if stdin, ok := cobraCommand.InOrStdin().(*os.File); ok { + size, err := pty.GetsizeFull(stdin) + if err != nil { + return errors.WithMessage(err, "failed to get terminal size") + } + + opts.Winsize = &runnerv2.Winsize{ + Rows: uint32(size.Rows), + Cols: uint32(size.Cols), + X: uint32(size.X), + Y: uint32(size.Y), + } + } + + return client.ExecuteProgram(ctx, cfg, opts) +} diff --git a/internal/command/command_terminal_test.go b/internal/command/command_terminal_test.go index 178f4ccbe..80278581e 100644 --- a/internal/command/command_terminal_test.go +++ b/internal/command/command_terminal_test.go @@ -52,12 +52,12 @@ func TestTerminalCommand_EnvPropagation(t *testing.T) { // Terminal command sets up a trap on EXIT. // Wait for it before starting to send commands. - expectContainLines(t, stdout, []string{"trap -- \"__cleanup\" EXIT"}) + expectContainLines(ctx, t, stdout, []string{"trap -- \"__cleanup\" EXIT"}) _, err = stdinW.Write([]byte("export TEST_ENV=1\n")) require.NoError(t, err) // Wait for the prompt before sending the next command. - expectContainLines(t, stdout, []string{"$"}) + expectContainLines(ctx, t, stdout, []string{"$"}) _, err = stdinW.Write([]byte("exit\n")) require.NoError(t, err) @@ -96,15 +96,19 @@ func TestTerminalCommand_Intro(t *testing.T) { require.NoError(t, cmd.Start(ctx)) - expectContainLines(t, stdout, []string{envSourceCmd, introSecondLine}) + expectContainLines(ctx, t, stdout, []string{envSourceCmd, introSecondLine}) } -func expectContainLines(t *testing.T, r io.Reader, expected []string) { +func expectContainLines(ctx context.Context, t *testing.T, r io.Reader, expected []string) { t.Helper() - var output strings.Builder + hits := make(map[string]bool, len(expected)) + output := new(strings.Builder) for { + buf := new(bytes.Buffer) + r := io.TeeReader(r, buf) + scanner := bufio.NewScanner(r) for scanner.Scan() { _, _ = output.WriteString(scanner.Text()) @@ -121,7 +125,11 @@ func expectContainLines(t *testing.T, r io.Reader, expected []string) { return } - time.Sleep(time.Millisecond * 400) + select { + case <-time.After(100 * time.Millisecond): + case <-ctx.Done(): + t.Fatalf("error waiting for line %q, instead read %q: %s", expected, buf.Bytes(), ctx.Err()) + } } } diff --git a/internal/command/command_virtual.go b/internal/command/command_virtual.go index bc62333fb..5b726cfd7 100644 --- a/internal/command/command_virtual.go +++ b/internal/command/command_virtual.go @@ -5,7 +5,6 @@ import ( "io" "os" "os/exec" - "reflect" "sync" "syscall" @@ -224,21 +223,6 @@ func SetWinsize(cmd Command, winsize *Winsize) (err error) { return errors.WithStack(err) } -func isNil(val any) bool { - if val == nil { - return true - } - - v := reflect.ValueOf(val) - - switch v.Type().Kind() { - case reflect.Chan, reflect.Func, reflect.Map, reflect.Pointer, reflect.UnsafePointer: - return v.IsNil() - default: - return false - } -} - // readCloser wraps [io.Reader] into [io.ReadCloser]. // // When Close is called, the underlying read operation is ignored. diff --git a/internal/command/command_windows.go b/internal/command/command_windows.go index 59b748999..7a8ca640d 100644 --- a/internal/command/command_windows.go +++ b/internal/command/command_windows.go @@ -28,4 +28,4 @@ func disableEcho(uintptr) error { "and join our Discord server at https://discord.gg/runme if you have further questions!") } -func signalPgid(int, os.Signal) error { return errors.New("unsupported") } +func signalPgid(int, os.Signal) error { return errors.New("signalPgid: unsupported") } diff --git a/internal/command/config_code_block.go b/internal/command/config_code_block.go index 44344ffb2..01040b222 100644 --- a/internal/command/config_code_block.go +++ b/internal/command/config_code_block.go @@ -9,12 +9,30 @@ import ( "github.com/stateful/runme/v3/pkg/document" ) -func NewProgramConfigFromCodeBlock(block *document.CodeBlock) (*ProgramConfig, error) { - return (&configBuilder{block: block}).Build() +type ConfigBuilderOption func(*configBuilder) error + +func WithInteractiveLegacy() ConfigBuilderOption { + return func(b *configBuilder) error { + b.useInteractiveLegacy = true + return nil + } +} + +func NewProgramConfigFromCodeBlock(block *document.CodeBlock, opts ...ConfigBuilderOption) (*ProgramConfig, error) { + b := &configBuilder{block: block} + + for _, opt := range opts { + if err := opt(b); err != nil { + return nil, err + } + } + + return b.Build() } type configBuilder struct { - block *document.CodeBlock + block *document.CodeBlock + useInteractiveLegacy bool } func (b *configBuilder) Build() (*ProgramConfig, error) { @@ -22,7 +40,12 @@ func (b *configBuilder) Build() (*ProgramConfig, error) { ProgramName: b.programPath(), LanguageId: b.block.Language(), Directory: b.dir(), - Interactive: b.block.Interactive(), + } + + if b.useInteractiveLegacy { + cfg.Interactive = b.block.InteractiveLegacy() + } else { + cfg.Interactive = b.block.Interactive() } if isShell(cfg) { diff --git a/internal/command/factory.go b/internal/command/factory.go index 1338c9761..8d996a281 100644 --- a/internal/command/factory.go +++ b/internal/command/factory.go @@ -2,6 +2,7 @@ package command import ( "io" + "reflect" "go.uber.org/zap" @@ -303,3 +304,18 @@ func (f *commandFactory) getLogger(name string) *zap.Logger { id := ulid.GenerateID() return f.logger.Named(name).With(zap.String("instanceID", id)) } + +func isNil(val any) bool { + if val == nil { + return true + } + + v := reflect.ValueOf(val) + + switch v.Type().Kind() { + case reflect.Chan, reflect.Func, reflect.Map, reflect.Pointer, reflect.UnsafePointer: + return v.IsNil() + default: + return false + } +} diff --git a/internal/config/autoconfig/autoconfig.go b/internal/config/autoconfig/autoconfig.go index 9ab399e81..7d9353e1e 100644 --- a/internal/config/autoconfig/autoconfig.go +++ b/internal/config/autoconfig/autoconfig.go @@ -98,6 +98,7 @@ func getClient(cfg *config.Config, logger *zap.Logger) (*runnerv2client.Client, return runnerv2client.New( cfg.Server.Address, + logger, opts..., ) } @@ -185,6 +186,10 @@ func getProject(c *config.Config, logger *zap.Logger) (*project.Project, error) project.WithLogger(logger), } + if env := c.Project.Env; env != nil { + opts = append(opts, project.WithEnvFilesReadOrder(env.Sources)) + } + if c.Project.Filename != "" { return project.NewFileProject(c.Project.Filename, opts...) } @@ -199,7 +204,6 @@ func getProject(c *config.Config, logger *zap.Logger) (*project.Project, error) opts, project.WithIgnoreFilePatterns(c.Project.Ignore...), project.WithRespectGitignore(!c.Project.DisableGitignore), - project.WithEnvFilesReadOrder(c.Project.Env.Sources), ) if c.Project.FindRepoUpward { diff --git a/internal/project/projectservice/project_service.go b/internal/project/projectservice/project_service.go index ae45c1d4a..af4e9b24b 100644 --- a/internal/project/projectservice/project_service.go +++ b/internal/project/projectservice/project_service.go @@ -6,6 +6,8 @@ import ( "sync" "go.uber.org/zap" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" projectv1 "github.com/stateful/runme/v3/pkg/api/gen/proto/go/runme/project/v1" "github.com/stateful/runme/v3/pkg/project" @@ -36,34 +38,45 @@ func (s *projectServiceServer) Load(req *projectv1.LoadRequest, srv projectv1.Pr go func() { defer wg.Done() defer close(errc) - for event := range eventc { - msg := &projectv1.LoadResponse{ - Type: projectv1.LoadEventType(event.Type), - } - - if err := setDataForLoadResponseFromLoadEvent(msg, event); err != nil { - errc <- err - goto errhandler - } - - if err := srv.Send(msg); err != nil { - errc <- err - goto errhandler - } - continue - - errhandler: - cancel() - // Project.Load() should be notified that it should exit early - // via cancel(). eventc will be closed, but it should be drained too - // in order to clean up any in-flight events. - // In theory, this is not necessary provided that all sends to eventc - // are wrapped in selects which observe ctx.Done(). - //revive:disable:empty-block - for range eventc { + for { + select { + case <-ctx.Done(): + errc <- status.Error(codes.Canceled, ctx.Err().Error()) + return + case event, ok := <-eventc: + if !ok { + return + } + + msg := &projectv1.LoadResponse{ + Type: projectv1.LoadEventType(event.Type), + } + + if err := setDataForLoadResponseFromLoadEvent(msg, event); err != nil { + errc <- err + goto errhandler + } + + if err := srv.Send(msg); err != nil { + errc <- err + goto errhandler + } + + continue + + errhandler: + cancel() + // Project.Load() should be notified that it should exit early + // via cancel(). eventc will be closed, but it should be drained too + // in order to clean up any in-flight events. + // In theory, this is not necessary provided that all sends to eventc + // are wrapped in selects which observe ctx.Done(). + //revive:disable:empty-block + for range eventc { + } + //revive:enable:empty-block } - //revive:enable:empty-block } }() diff --git a/internal/project/projectservice/project_service_test.go b/internal/project/projectservice/project_service_test.go index 4675bac05..61df0bb36 100644 --- a/internal/project/projectservice/project_service_test.go +++ b/internal/project/projectservice/project_service_test.go @@ -9,30 +9,26 @@ import ( "github.com/pkg/errors" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "go.uber.org/zap" + "go.uber.org/zap/zaptest" "google.golang.org/grpc" "google.golang.org/grpc/codes" - "google.golang.org/grpc/credentials/insecure" - "google.golang.org/grpc/resolver" "google.golang.org/grpc/status" "google.golang.org/grpc/test/bufconn" "github.com/stateful/runme/v3/internal/project/projectservice" + "github.com/stateful/runme/v3/internal/testutils" projectv1 "github.com/stateful/runme/v3/pkg/api/gen/proto/go/runme/project/v1" "github.com/stateful/runme/v3/pkg/project/teststub" - "github.com/stateful/runme/v3/pkg/project/testutils" + projtestutils "github.com/stateful/runme/v3/pkg/project/testutils" ) -func init() { - resolver.SetDefaultScheme("passthrough") -} - func TestProjectServiceServer_Load(t *testing.T) { t.Parallel() lis, stop := testStartProjectServiceServer(t) t.Cleanup(stop) - _, client := testCreateProjectServiceClient(t, lis) + + _, client := testutils.NewTestGRPCClient(t, lis, projectv1.NewProjectServiceClient) t.Run("GitProject", func(t *testing.T) { t.Parallel() @@ -45,7 +41,7 @@ func TestProjectServiceServer_Load(t *testing.T) { Directory: &projectv1.DirectoryProjectOptions{ Path: testData.GitProjectPath(), SkipGitignore: false, - IgnoreFilePatterns: testutils.IgnoreFilePatternsWithDefaults("ignored.md"), + IgnoreFilePatterns: projtestutils.IgnoreFilePatternsWithDefaults("ignored.md"), SkipRepoLookupUpward: false, }, }, @@ -56,7 +52,7 @@ func TestProjectServiceServer_Load(t *testing.T) { eventTypes, err := collectLoadEventTypes(loadClient) require.NoError(t, err) - assert.Len(t, eventTypes, len(testutils.GitProjectLoadOnlyNotIgnoredFilesEvents)) + assert.Len(t, eventTypes, len(projtestutils.GitProjectLoadOnlyNotIgnoredFilesEvents)) }) t.Run("FileProject", func(t *testing.T) { @@ -78,7 +74,7 @@ func TestProjectServiceServer_Load(t *testing.T) { eventTypes, err := collectLoadEventTypes(loadClient) require.NoError(t, err) - assert.Len(t, eventTypes, len(testutils.FileProjectEvents)) + assert.Len(t, eventTypes, len(projtestutils.FileProjectEvents)) }) } @@ -90,7 +86,8 @@ func TestProjectServiceServer_Load_ClientConnClosed(t *testing.T) { lis, stop := testStartProjectServiceServer(t) t.Cleanup(stop) - clientConn, client := testCreateProjectServiceClient(t, lis) + + clientConn, client := testutils.NewTestGRPCClient(t, lis, projectv1.NewProjectServiceClient) req := &projectv1.LoadRequest{ Kind: &projectv1.LoadRequest_File{ @@ -99,14 +96,11 @@ func TestProjectServiceServer_Load_ClientConnClosed(t *testing.T) { }, }, } - loadClient, err := client.Load(context.Background(), req) require.NoError(t, err) - errc := make(chan error, 1) - go func() { - errc <- clientConn.Close() - }() + err = clientConn.Close() + require.NoError(t, err) for { _, err := loadClient.Recv() @@ -115,8 +109,6 @@ func TestProjectServiceServer_Load_ClientConnClosed(t *testing.T) { break } } - - require.NoError(t, <-errc) } func collectLoadEventTypes(client projectv1.ProjectService_LoadClient) ([]projectv1.LoadEventType, error) { @@ -140,31 +132,14 @@ func testStartProjectServiceServer(t *testing.T) ( interface{ Dial() (net.Conn, error) }, func(), ) { - logger, err := zap.NewDevelopment() - require.NoError(t, err) + t.Helper() server := grpc.NewServer() - service := projectservice.NewProjectServiceServer(logger) + + service := projectservice.NewProjectServiceServer(zaptest.NewLogger(t)) projectv1.RegisterProjectServiceServer(server, service) lis := bufconn.Listen(1024 << 10) - go server.Serve(lis) - return lis, server.Stop } - -func testCreateProjectServiceClient( - t *testing.T, - lis interface{ Dial() (net.Conn, error) }, -) (*grpc.ClientConn, projectv1.ProjectServiceClient) { - conn, err := grpc.NewClient( - "passthrough", - grpc.WithTransportCredentials(insecure.NewCredentials()), - grpc.WithContextDialer(func(ctx context.Context, s string) (net.Conn, error) { - return lis.Dial() - }), - ) - require.NoError(t, err) - return conn, projectv1.NewProjectServiceClient(conn) -} diff --git a/internal/runner/service_test.go b/internal/runner/service_test.go index 988fd8019..d63518777 100644 --- a/internal/runner/service_test.go +++ b/internal/runner/service_test.go @@ -20,26 +20,24 @@ import ( "go.uber.org/zap" "google.golang.org/grpc" "google.golang.org/grpc/codes" - "google.golang.org/grpc/credentials/insecure" - "google.golang.org/grpc/resolver" "google.golang.org/grpc/status" "google.golang.org/grpc/test/bufconn" "google.golang.org/protobuf/proto" + "github.com/stateful/runme/v3/internal/testutils" "github.com/stateful/runme/v3/internal/ulid" runnerv1 "github.com/stateful/runme/v3/pkg/api/gen/proto/go/runme/runner/v1" ) +// TODO(adamb): remove global state. It prevents from running tests in parallel. var ( logger *zap.Logger logFile string ) -func init() { - resolver.SetDefaultScheme("passthrough") -} - +// TODO(adamb): remove and use [zaptest.NewLogger] instead. func testCreateLogger(t *testing.T) *zap.Logger { + t.Helper() logger, err := zap.NewDevelopment() require.NoError(t, err) t.Cleanup(func() { _ = logger.Sync() }) @@ -75,21 +73,6 @@ func testStartRunnerServiceServer(t *testing.T) ( return lis, server.Stop } -func testCreateRunnerServiceClient( - t *testing.T, - lis interface{ Dial() (net.Conn, error) }, -) (*grpc.ClientConn, runnerv1.RunnerServiceClient) { - conn, err := grpc.NewClient( - "passthrough", - grpc.WithTransportCredentials(insecure.NewCredentials()), - grpc.WithContextDialer(func(ctx context.Context, s string) (net.Conn, error) { - return lis.Dial() - }), - ) - require.NoError(t, err) - return conn, runnerv1.NewRunnerServiceClient(conn) -} - type executeResult struct { Stdout []byte Stderr []byte @@ -129,7 +112,8 @@ func getExecuteResult( func Test_runnerService(t *testing.T) { lis, stop := testStartRunnerServiceServer(t) t.Cleanup(stop) - _, client := testCreateRunnerServiceClient(t, lis) + + _, client := testutils.NewTestGRPCClient(t, lis, runnerv1.NewRunnerServiceClient) t.Run("Sessions", func(t *testing.T) { t.Parallel() diff --git a/internal/runnerv2client/client.go b/internal/runnerv2client/client.go index d253c859c..d0e6305e3 100644 --- a/internal/runnerv2client/client.go +++ b/internal/runnerv2client/client.go @@ -1,22 +1,164 @@ package runnerv2client import ( + "context" + "io" + "reflect" + "github.com/pkg/errors" - runnerv2 "github.com/stateful/runme/v3/pkg/api/gen/proto/go/runme/runner/v2" + "go.uber.org/zap" "google.golang.org/grpc" + + runnerv2 "github.com/stateful/runme/v3/pkg/api/gen/proto/go/runme/runner/v2" ) type Client struct { runnerv2.RunnerServiceClient + conn *grpc.ClientConn + logger *zap.Logger } -func New(target string, opts ...grpc.DialOption) (*Client, error) { +func New(target string, logger *zap.Logger, opts ...grpc.DialOption) (*Client, error) { client, err := grpc.NewClient(target, opts...) if err != nil { return nil, errors.WithStack(err) } + serviceClient := &Client{ + RunnerServiceClient: runnerv2.NewRunnerServiceClient(client), + conn: client, + logger: logger, + } + return serviceClient, nil +} - serviceClient := &Client{RunnerServiceClient: runnerv2.NewRunnerServiceClient(client)} +func (c *Client) Close() error { + return c.conn.Close() +} - return serviceClient, nil +type ExecuteProgramOptions struct { + InputData []byte + SessionID string + Stdin io.ReadCloser + Stdout io.Writer + Stderr io.Writer + StoreStdoutInEnv bool + Winsize *runnerv2.Winsize +} + +func (c *Client) ExecuteProgram( + ctx context.Context, + cfg *runnerv2.ProgramConfig, + opts ExecuteProgramOptions, +) error { + return c.executeProgram(ctx, cfg, opts) +} + +func (c *Client) executeProgram( + ctx context.Context, + cfg *runnerv2.ProgramConfig, + opts ExecuteProgramOptions, +) error { + stream, err := c.Execute(ctx) + if err != nil { + return errors.WithMessage(err, "failed to call Execute()") + } + + // Send the initial request. + req := &runnerv2.ExecuteRequest{ + Config: cfg, + InputData: opts.InputData, + SessionId: opts.SessionID, + StoreStdoutInEnv: opts.StoreStdoutInEnv, + Winsize: opts.Winsize, + } + if err := stream.Send(req); err != nil { + return errors.WithMessage(err, "failed to send initial request") + } + + if stdin := opts.Stdin; !isNil(stdin) { + // TODO(adamb): reimplement it. There should be a singleton + // handling and forwarding the stdin. The current implementation + // does not temrinate multiple stdin readers in the case of + // running multiple commands using "beta run command1 command2 ... commandN". + go func() { + defer func() { + c.logger.Info("finishing reading stdin") + err := stream.CloseSend() + if err != nil { + c.logger.Info("failed to close send", zap.Error(err)) + } + }() + + c.logger.Info("reading stdin") + + buf := make([]byte, 2*1024*1024) + + for { + n, err := stdin.Read(buf) + if err != nil { + c.logger.Info("failed to read stdin", zap.Error(err)) + break + } + + c.logger.Info("sending stdin", zap.Int("size", n)) + + err = stream.Send(&runnerv2.ExecuteRequest{ + InputData: buf[:n], + }) + if err != nil { + c.logger.Info("failed to send stdin", zap.Error(err)) + break + } + } + }() + } + + for { + resp, err := stream.Recv() + if err != nil { + if !errors.Is(err, io.EOF) { + c.logger.Info("failed to receive response", zap.Error(err)) + } + break + } + + if pid := resp.Pid; pid != nil { + c.logger.Info("server started a process with PID", zap.Uint32("pid", pid.GetValue()), zap.String("mime", resp.MimeType)) + } + + if stdout := opts.Stdout; !isNil(stdout) { + _, err = stdout.Write(resp.StdoutData) + if err != nil { + return errors.WithMessage(err, "failed to write stdout") + } + } + + if stderr := opts.Stderr; !isNil(stderr) { + _, err = stderr.Write(resp.StderrData) + if err != nil { + return errors.WithMessage(err, "failed to write stderr") + } + } + + if code := resp.GetExitCode(); code != nil && code.GetValue() != 0 { + return errors.WithMessagef(err, "exit with code %d", code.GetValue()) + } + } + + return nil +} + +func isNil(val any) bool { + if val == nil { + return true + } + + v := reflect.ValueOf(val) + + switch v.Type().Kind() { + case reflect.Chan, reflect.Func, reflect.Map, reflect.Pointer, reflect.UnsafePointer: + return v.IsNil() + default: + return false + } } diff --git a/internal/runnerv2client/client_test.go b/internal/runnerv2client/client_test.go new file mode 100644 index 000000000..ad99a050b --- /dev/null +++ b/internal/runnerv2client/client_test.go @@ -0,0 +1,137 @@ +package runnerv2client + +import ( + "bytes" + "context" + "net" + "testing" + "time" + + "github.com/stretchr/testify/require" + "go.uber.org/zap/zaptest" + "google.golang.org/grpc" + "google.golang.org/grpc/credentials/insecure" + "google.golang.org/grpc/test/bufconn" + + "github.com/stateful/runme/v3/internal/command" + "github.com/stateful/runme/v3/internal/runnerv2service" + runnerv2 "github.com/stateful/runme/v3/pkg/api/gen/proto/go/runme/runner/v2" +) + +func init() { + command.SetEnvDumpCommand("env -0") +} + +func TestClient_ExecuteProgram(t *testing.T) { + t.Parallel() + + lis, stop := testStartRunnerServiceServer(t) + t.Cleanup(stop) + + t.Run("OutputWithSession", func(t *testing.T) { + t.Parallel() + + client := testCreateClient(t, lis) + ctx, cancel := context.WithTimeout(context.Background(), time.Second*10) + defer cancel() + + sessionResp, err := client.CreateSession( + ctx, + &runnerv2.CreateSessionRequest{ + Env: []string{"TEST=test-output-with-session-env"}, + }, + ) + require.NoError(t, err) + + cfg := &command.ProgramConfig{ + ProgramName: "bash", + Source: &runnerv2.ProgramConfig_Commands{ + Commands: &runnerv2.ProgramConfig_CommandList{ + Items: []string{ + "echo -n $TEST", + ">&2 echo -n test-output-with-session-stderr", + }, + }, + }, + Mode: runnerv2.CommandMode_COMMAND_MODE_INLINE, + } + stdout := new(bytes.Buffer) + stderr := new(bytes.Buffer) + err = client.ExecuteProgram( + ctx, + cfg, + ExecuteProgramOptions{ + SessionID: sessionResp.GetSession().GetId(), + Stdout: stdout, + Stderr: stderr, + }, + ) + require.NoError(t, err) + require.Equal(t, "test-output-with-session-env", stdout.String()) + require.Equal(t, "test-output-with-session-stderr", stderr.String()) + }) + + t.Run("InputNonInteractive", func(t *testing.T) { + t.Parallel() + + client := testCreateClient(t, lis) + ctx, cancel := context.WithTimeout(context.Background(), time.Second*10) + defer cancel() + + cfg := &command.ProgramConfig{ + ProgramName: "bash", + Source: &runnerv2.ProgramConfig_Commands{ + Commands: &runnerv2.ProgramConfig_CommandList{ + Items: []string{ + "read -r name", + "echo $name", + }, + }, + }, + Mode: runnerv2.CommandMode_COMMAND_MODE_INLINE, + } + stdout := new(bytes.Buffer) + err := client.ExecuteProgram( + ctx, + cfg, + ExecuteProgramOptions{ + InputData: []byte("test-input-non-interactive\n"), + Stdout: stdout, + }, + ) + require.NoError(t, err) + require.Equal(t, "test-input-non-interactive\n", stdout.String()) + }) +} + +// TODO(adamb): it's copied from internal/runnerv2service. +func testStartRunnerServiceServer(t *testing.T) (*bufconn.Listener, func()) { + t.Helper() + + logger := zaptest.NewLogger(t) + factory := command.NewFactory(command.WithLogger(logger)) + + server := grpc.NewServer() + + runnerService, err := runnerv2service.NewRunnerService(factory, logger) + require.NoError(t, err) + runnerv2.RegisterRunnerServiceServer(server, runnerService) + + lis := bufconn.Listen(1024 << 10) + go server.Serve(lis) + + return lis, server.Stop +} + +func testCreateClient(t *testing.T, lis *bufconn.Listener) *Client { + client, err := New( + "passthrough://bufconn", + zaptest.NewLogger(t), + grpc.WithContextDialer(func(context.Context, string) (net.Conn, error) { + return lis.Dial() + }), + grpc.WithTransportCredentials(insecure.NewCredentials()), + ) + require.NoError(t, err) + return client +} diff --git a/internal/runnerv2client/client_unix_test.go b/internal/runnerv2client/client_unix_test.go new file mode 100644 index 000000000..05cf77c44 --- /dev/null +++ b/internal/runnerv2client/client_unix_test.go @@ -0,0 +1,54 @@ +//go:build !windows +// +build !windows + +package runnerv2client + +import ( + "bytes" + "context" + "io" + "testing" + "time" + + "github.com/stateful/runme/v3/internal/command" + runnerv2 "github.com/stateful/runme/v3/pkg/api/gen/proto/go/runme/runner/v2" + "github.com/stretchr/testify/require" +) + +func TestClient_ExecuteProgram_InputInteractive(t *testing.T) { + t.Parallel() + + lis, stop := testStartRunnerServiceServer(t) + t.Cleanup(stop) + + client := testCreateClient(t, lis) + ctx, cancel := context.WithTimeout(context.Background(), time.Second*10) + defer cancel() + + cfg := &command.ProgramConfig{ + ProgramName: "bash", + Source: &runnerv2.ProgramConfig_Commands{ + Commands: &runnerv2.ProgramConfig_CommandList{ + Items: []string{ + "read -r name", + "echo $name", + }, + }, + }, + Interactive: true, + Mode: runnerv2.CommandMode_COMMAND_MODE_INLINE, + } + stdout := new(bytes.Buffer) + err := client.ExecuteProgram( + ctx, + cfg, + ExecuteProgramOptions{ + Stdin: io.NopCloser(bytes.NewBufferString("test-input-interactive\n")), + Stdout: stdout, + }, + ) + require.NoError(t, err) + // Using [require.Contains] because on Linux the input is repeated. + // Unclear why it passes fine on macOS. + require.Contains(t, stdout.String(), "test-input-interactive\r\n") +} diff --git a/internal/runnerv2service/execution.go b/internal/runnerv2service/execution.go index 7328ce02f..d3100622b 100644 --- a/internal/runnerv2service/execution.go +++ b/internal/runnerv2service/execution.go @@ -29,7 +29,7 @@ const ( // small. // In the future, it might be worth to implement // variable-sized buffers. - msgBufferSize = 2048 << 10 // 2 MiB + msgBufferSize = 2 * 1024 * 1024 // 2 MiB ) var opininatedEnvVarNamingRegexp = regexp.MustCompile(`^[A-Z_][A-Z0-9_]{1}[A-Z0-9_]*[A-Z][A-Z0-9_]*$`) diff --git a/internal/runnerv2service/service_execute_test.go b/internal/runnerv2service/service_execute_test.go index 3957b1f3a..20ad89a34 100644 --- a/internal/runnerv2service/service_execute_test.go +++ b/internal/runnerv2service/service_execute_test.go @@ -18,22 +18,19 @@ import ( "github.com/stretchr/testify/require" "go.uber.org/zap/zaptest" "google.golang.org/grpc" - "google.golang.org/grpc/credentials/insecure" - "google.golang.org/grpc/resolver" "google.golang.org/grpc/test/bufconn" "github.com/stateful/runme/v3/internal/command" "github.com/stateful/runme/v3/internal/command/testdata" "github.com/stateful/runme/v3/internal/config" "github.com/stateful/runme/v3/internal/config/autoconfig" + "github.com/stateful/runme/v3/internal/testutils" runnerv2 "github.com/stateful/runme/v3/pkg/api/gen/proto/go/runme/runner/v2" ) func init() { command.SetEnvDumpCommand("env -0") - resolver.SetDefaultScheme("passthrough") - // Server uses autoconfig to get necessary dependencies. // One of them, implicit, is [config.Config]. With the default // [config.Loader] it won't be found during testing, so @@ -89,7 +86,7 @@ func Test_conformsOpinionatedEnvVarNaming(t *testing.T) { func TestRunnerServiceServerExecute_Response(t *testing.T) { lis, stop := testStartRunnerServiceServer(t) t.Cleanup(stop) - _, client := testCreateRunnerServiceClient(t, lis) + _, client := testutils.NewTestGRPCClient(t, lis, runnerv2.NewRunnerServiceClient) stream, err := client.Execute(context.Background()) require.NoError(t, err) @@ -160,7 +157,7 @@ func TestRunnerServiceServerExecute_Response(t *testing.T) { func TestRunnerServiceServerExecute_MimeType(t *testing.T) { lis, stop := testStartRunnerServiceServer(t) t.Cleanup(stop) - _, client := testCreateRunnerServiceClient(t, lis) + _, client := testutils.NewTestGRPCClient(t, lis, runnerv2.NewRunnerServiceClient) stream, err := client.Execute(context.Background()) require.NoError(t, err) @@ -199,7 +196,7 @@ func TestRunnerServiceServerExecute_MimeType(t *testing.T) { func TestRunnerServiceServerExecute_StoreLastStdout(t *testing.T) { lis, stop := testStartRunnerServiceServer(t) t.Cleanup(stop) - _, client := testCreateRunnerServiceClient(t, lis) + _, client := testutils.NewTestGRPCClient(t, lis, runnerv2.NewRunnerServiceClient) sessionResp, err := client.CreateSession(context.Background(), &runnerv2.CreateSessionRequest{}) require.NoError(t, err) @@ -276,7 +273,7 @@ func TestRunnerServiceServerExecute_LastStdoutExceedsEnvLimit(t *testing.T) { lis, stop := testStartRunnerServiceServer(t) t.Cleanup(stop) - _, client := testCreateRunnerServiceClient(t, lis) + _, client := testutils.NewTestGRPCClient(t, lis, runnerv2.NewRunnerServiceClient) sessionResp, err := client.CreateSession(context.Background(), &runnerv2.CreateSessionRequest{}) require.NoError(t, err) @@ -353,7 +350,7 @@ func TestRunnerServiceServerExecute_LargeOutput(t *testing.T) { lis, stop := testStartRunnerServiceServer(t) t.Cleanup(stop) - _, client := testCreateRunnerServiceClient(t, lis) + _, client := testutils.NewTestGRPCClient(t, lis, runnerv2.NewRunnerServiceClient) stream, err := client.Execute(context.Background()) require.NoError(t, err) @@ -387,7 +384,7 @@ func TestRunnerServiceServerExecute_LargeOutput(t *testing.T) { func TestRunnerServiceServerExecute_StoreKnownName(t *testing.T) { lis, stop := testStartRunnerServiceServer(t) t.Cleanup(stop) - _, client := testCreateRunnerServiceClient(t, lis) + _, client := testutils.NewTestGRPCClient(t, lis, runnerv2.NewRunnerServiceClient) sessionResp, err := client.CreateSession(context.Background(), &runnerv2.CreateSessionRequest{}) require.NoError(t, err) @@ -460,7 +457,7 @@ func TestRunnerServiceServerExecute_StoreKnownName(t *testing.T) { func TestRunnerServiceServerExecute_Configs(t *testing.T) { lis, stop := testStartRunnerServiceServer(t) t.Cleanup(stop) - _, client := testCreateRunnerServiceClient(t, lis) + _, client := testutils.NewTestGRPCClient(t, lis, runnerv2.NewRunnerServiceClient) testCases := []struct { name string @@ -617,7 +614,7 @@ func TestRunnerServiceServerExecute_Configs(t *testing.T) { func TestRunnerServiceServerExecute_CommandMode_Terminal(t *testing.T) { lis, stop := testStartRunnerServiceServer(t) t.Cleanup(stop) - _, client := testCreateRunnerServiceClient(t, lis) + _, client := testutils.NewTestGRPCClient(t, lis, runnerv2.NewRunnerServiceClient) sessResp, err := client.CreateSession(context.Background(), &runnerv2.CreateSessionRequest{}) require.NoError(t, err) @@ -696,7 +693,7 @@ func TestRunnerServiceServerExecute_CommandMode_Terminal(t *testing.T) { func TestRunnerServiceServerExecute_PathEnvInSession(t *testing.T) { lis, stop := testStartRunnerServiceServer(t) t.Cleanup(stop) - _, client := testCreateRunnerServiceClient(t, lis) + _, client := testutils.NewTestGRPCClient(t, lis, runnerv2.NewRunnerServiceClient) sessionResp, err := client.CreateSession(context.Background(), &runnerv2.CreateSessionRequest{}) require.NoError(t, err) @@ -759,7 +756,7 @@ func TestRunnerServiceServerExecute_PathEnvInSession(t *testing.T) { func TestRunnerServiceServerExecute_WithInput(t *testing.T) { lis, stop := testStartRunnerServiceServer(t) t.Cleanup(stop) - _, client := testCreateRunnerServiceClient(t, lis) + _, client := testutils.NewTestGRPCClient(t, lis, runnerv2.NewRunnerServiceClient) t.Run("ContinuousInput", func(t *testing.T) { stream, err := client.Execute(context.Background()) @@ -876,7 +873,7 @@ func TestRunnerServiceServerExecute_WithInput(t *testing.T) { func TestRunnerServiceServerExecute_WithSession(t *testing.T) { lis, stop := testStartRunnerServiceServer(t) t.Cleanup(stop) - _, client := testCreateRunnerServiceClient(t, lis) + _, client := testutils.NewTestGRPCClient(t, lis, runnerv2.NewRunnerServiceClient) t.Run("WithEnvAndMostRecentSessionStrategy", func(t *testing.T) { { @@ -942,7 +939,7 @@ func TestRunnerServiceServerExecute_WithSession(t *testing.T) { func TestRunnerServiceServerExecute_WithStop(t *testing.T) { lis, stop := testStartRunnerServiceServer(t) t.Cleanup(stop) - _, client := testCreateRunnerServiceClient(t, lis) + _, client := testutils.NewTestGRPCClient(t, lis, runnerv2.NewRunnerServiceClient) stream, err := client.Execute(context.Background()) require.NoError(t, err) @@ -1009,7 +1006,8 @@ func TestRunnerServiceServerExecute_WithStop(t *testing.T) { func TestRunnerServiceServerExecute_Winsize(t *testing.T) { lis, stop := testStartRunnerServiceServer(t) t.Cleanup(stop) - _, client := testCreateRunnerServiceClient(t, lis) + + _, client := testutils.NewTestGRPCClient(t, lis, runnerv2.NewRunnerServiceClient) t.Run("DefaultWinsize", func(t *testing.T) { t.Parallel() @@ -1104,24 +1102,6 @@ func testStartRunnerServiceServer(t *testing.T) ( return lis, server.Stop } -func testCreateRunnerServiceClient( - t *testing.T, - lis interface{ Dial() (net.Conn, error) }, -) (*grpc.ClientConn, runnerv2.RunnerServiceClient) { - t.Helper() - - conn, err := grpc.NewClient( - "passthrough", - grpc.WithTransportCredentials(insecure.NewCredentials()), - grpc.WithContextDialer(func(ctx context.Context, s string) (net.Conn, error) { - return lis.Dial() - }), - ) - require.NoError(t, err) - - return conn, runnerv2.NewRunnerServiceClient(conn) -} - type executeResult struct { Stdout []byte Stderr []byte diff --git a/internal/runnerv2service/service_resolve_program_test.go b/internal/runnerv2service/service_resolve_program_test.go index 8ab8dcb5e..1ce0a5c36 100644 --- a/internal/runnerv2service/service_resolve_program_test.go +++ b/internal/runnerv2service/service_resolve_program_test.go @@ -9,6 +9,7 @@ import ( "github.com/stretchr/testify/require" "google.golang.org/protobuf/proto" + "github.com/stateful/runme/v3/internal/testutils" runnerv2 "github.com/stateful/runme/v3/pkg/api/gen/proto/go/runme/runner/v2" ) @@ -16,7 +17,7 @@ import ( func TestRunnerServiceResolveProgram(t *testing.T) { lis, stop := testStartRunnerServiceServer(t) t.Cleanup(stop) - _, client := testCreateRunnerServiceClient(t, lis) + _, client := testutils.NewTestGRPCClient(t, lis, runnerv2.NewRunnerServiceClient) testCases := []struct { name string @@ -93,7 +94,7 @@ func TestRunnerResolveProgram_CommandsWithNewLines(t *testing.T) { lis, stop := testStartRunnerServiceServer(t) t.Cleanup(stop) - _, client := testCreateRunnerServiceClient(t, lis) + _, client := testutils.NewTestGRPCClient(t, lis, runnerv2.NewRunnerServiceClient) request := &runnerv2.ResolveProgramRequest{ Env: []string{"FILE_NAME=my-file.txt"}, @@ -145,7 +146,7 @@ func TestRunnerResolveProgram_CommandsWithNewLines(t *testing.T) { func TestRunnerResolveProgram_OnlyShellLanguages(t *testing.T) { lis, stop := testStartRunnerServiceServer(t) t.Cleanup(stop) - _, client := testCreateRunnerServiceClient(t, lis) + _, client := testutils.NewTestGRPCClient(t, lis, runnerv2.NewRunnerServiceClient) t.Run("Javascript passed as script", func(t *testing.T) { script := "console.log('test');" diff --git a/internal/runnerv2service/service_sessions_test.go b/internal/runnerv2service/service_sessions_test.go index ff51e4e83..43d6727ae 100644 --- a/internal/runnerv2service/service_sessions_test.go +++ b/internal/runnerv2service/service_sessions_test.go @@ -10,6 +10,7 @@ import ( "github.com/stretchr/testify/require" "google.golang.org/grpc/resolver" + "github.com/stateful/runme/v3/internal/testutils" runnerv2 "github.com/stateful/runme/v3/pkg/api/gen/proto/go/runme/runner/v2" "github.com/stateful/runme/v3/pkg/project/teststub" ) @@ -25,7 +26,8 @@ func TestRunnerServiceSessions(t *testing.T) { lis, stop := testStartRunnerServiceServer(t) t.Cleanup(stop) - _, client := testCreateRunnerServiceClient(t, lis) + + _, client := testutils.NewTestGRPCClient(t, lis, runnerv2.NewRunnerServiceClient) EnvStoreSeedingNone := runnerv2.CreateSessionRequest_Config_SESSION_ENV_STORE_SEEDING_UNSPECIFIED.Enum() @@ -117,7 +119,8 @@ func TestRunnerServiceSessions(t *testing.T) { func TestRunnerServiceSessions_StrategyMostRecent(t *testing.T) { lis, stop := testStartRunnerServiceServer(t) t.Cleanup(stop) - _, client := testCreateRunnerServiceClient(t, lis) + + _, client := testutils.NewTestGRPCClient(t, lis, runnerv2.NewRunnerServiceClient) // Create a session with env. sessResp, err := client.CreateSession( diff --git a/internal/testutils/grpc.go b/internal/testutils/grpc.go new file mode 100644 index 000000000..266cdb359 --- /dev/null +++ b/internal/testutils/grpc.go @@ -0,0 +1,51 @@ +package testutils + +import ( + "context" + "net" + "testing" + + "github.com/stretchr/testify/require" + "google.golang.org/grpc" + "google.golang.org/grpc/credentials/insecure" +) + +func NewTestGRPCClient[T any]( + t *testing.T, + lis interface{ Dial() (net.Conn, error) }, + fn func(grpc.ClientConnInterface) T, +) (*grpc.ClientConn, T) { + t.Helper() + conn, client, err := newGRPCClient(lis, fn) + require.NoError(t, err) + return conn, client +} + +func NewGRPCClient[T any]( + lis interface{ Dial() (net.Conn, error) }, + fn func(grpc.ClientConnInterface) T, +) (*grpc.ClientConn, T) { + conn, client, err := newGRPCClient(lis, fn) + if err != nil { + panic(err) + } + return conn, client +} + +func newGRPCClient[T any]( + lis interface{ Dial() (net.Conn, error) }, + fn func(grpc.ClientConnInterface) T, +) (*grpc.ClientConn, T, error) { + conn, err := grpc.NewClient( + "passthrough://bufconn", + grpc.WithContextDialer(func(ctx context.Context, s string) (net.Conn, error) { + return lis.Dial() + }), + grpc.WithTransportCredentials(insecure.NewCredentials()), + ) + if err != nil { + var result T + return nil, result, err + } + return conn, fn(conn), nil +} diff --git a/internal/tls/tls.go b/internal/tls/tls.go index 2e6cfbc28..aecded448 100644 --- a/internal/tls/tls.go +++ b/internal/tls/tls.go @@ -91,7 +91,8 @@ func LoadOrGenerateConfig(certFile, keyFile string, logger *zap.Logger) (*tls.Co } if config != nil { - if ttl, err := validateTLSConfig(config); err == nil { + ttl, err := validateTLSConfig(config) + if err == nil { logger.Info("certificate is valid", zap.Duration("ttl", ttl), zap.String("certFile", certFile), zap.String("keyFile", keyFile)) return config, nil } diff --git a/pkg/document/block.go b/pkg/document/block.go index 8e104f0fe..9cf982309 100644 --- a/pkg/document/block.go +++ b/pkg/document/block.go @@ -176,7 +176,8 @@ func (b *CodeBlock) Interactive() bool { } // InteractiveLegacy returns true as a default value. -// Deprecated: use Interactive instead. +// Deprecated: use Interactive instead, however, keep using +// if you want to align with the VS Code extension. func (b *CodeBlock) InteractiveLegacy() bool { val, err := strconv.ParseBool(b.Attributes()["interactive"]) if err != nil { diff --git a/pkg/document/editor/editorservice/service_test.go b/pkg/document/editor/editorservice/service_test.go index fadf883fd..cc72e1665 100644 --- a/pkg/document/editor/editorservice/service_test.go +++ b/pkg/document/editor/editorservice/service_test.go @@ -3,19 +3,17 @@ package editorservice import ( "context" "fmt" - "net" "os" "strings" "testing" + "github.com/stateful/runme/v3/internal/testutils" "github.com/stateful/runme/v3/internal/ulid" "github.com/stateful/runme/v3/internal/version" parserv1 "github.com/stateful/runme/v3/pkg/api/gen/proto/go/runme/parser/v1" "github.com/stretchr/testify/assert" "go.uber.org/zap" "google.golang.org/grpc" - "google.golang.org/grpc/credentials/insecure" - "google.golang.org/grpc/resolver" "google.golang.org/grpc/test/bufconn" "google.golang.org/protobuf/proto" "google.golang.org/protobuf/types/known/wrapperspb" @@ -56,27 +54,15 @@ var ( func TestMain(m *testing.M) { ulid.MockGenerator(testMockID) - resolver.SetDefaultScheme("passthrough") lis := bufconn.Listen(2048) server := grpc.NewServer() parserv1.RegisterParserServiceServer(server, NewParserServiceServer(zap.NewNop())) go server.Serve(lis) - conn, err := grpc.NewClient( - "passthrough", - grpc.WithTransportCredentials(insecure.NewCredentials()), - grpc.WithContextDialer(func(ctx context.Context, s string) (net.Conn, error) { - return lis.Dial() - }), - ) - if err != nil { - panic(err) - } + _, client = testutils.NewGRPCClient(lis, parserv1.NewParserServiceClient) - client = parserv1.NewParserServiceClient(conn) code := m.Run() - ulid.ResetGenerator() os.Exit(code) }