Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(plugins): Wire the test verb up to the CLI plugin infrastructure #91

Merged
merged 2 commits into from
Jan 7, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion cmd/aspect/root/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ func NewRootCmd(
cmd.AddCommand(version.NewDefaultVersionCmd())
cmd.AddCommand(docs.NewDefaultDocsCmd())
cmd.AddCommand(info.NewDefaultInfoCmd())
cmd.AddCommand(test.NewDefaultTestCmd())
cmd.AddCommand(test.NewDefaultTestCmd(pluginSystem))

// ### "Additional help topic commands" which are not runnable
// https://pkg.go.dev/github.com/spf13/cobra#Command.IsAdditionalHelpTopicCommand
Expand Down
4 changes: 4 additions & 0 deletions cmd/aspect/test/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,14 @@ go_library(
importpath = "aspect.build/cli/cmd/aspect/test",
visibility = ["//visibility:public"],
deps = [
"//cmd/aspect/root/flags",
"//pkg/aspect/test",
"//pkg/aspecterrors",
"//pkg/bazel",
"//pkg/interceptors",
"//pkg/ioutils",
"//pkg/plugin/system",
"//pkg/plugin/system/bep",
"@com_github_spf13_cobra//:cobra",
],
)
44 changes: 40 additions & 4 deletions cmd/aspect/test/test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,20 +8,36 @@ package test

import (
"context"
"errors"
"fmt"

"github.com/spf13/cobra"

rootFlags "aspect.build/cli/cmd/aspect/root/flags"
"aspect.build/cli/pkg/aspect/test"
"aspect.build/cli/pkg/aspecterrors"
"aspect.build/cli/pkg/bazel"
"aspect.build/cli/pkg/interceptors"
"aspect.build/cli/pkg/ioutils"
"aspect.build/cli/pkg/plugin/system"
"aspect.build/cli/pkg/plugin/system/bep"
)

func NewDefaultTestCmd() *cobra.Command {
return NewTestCmd(ioutils.DefaultStreams, bazel.New())
// NewDefaultTestCmd creates a new build cobra command with the default
// dependencies.
func NewDefaultTestCmd(pluginSystem system.PluginSystem) *cobra.Command {
return NewTestCmd(
ioutils.DefaultStreams,
pluginSystem,
bazel.New(),
)
}

func NewTestCmd(streams ioutils.Streams, bzl bazel.Bazel) *cobra.Command {
func NewTestCmd(
streams ioutils.Streams,
pluginSystem system.PluginSystem,
bzl bazel.Bazel,
) *cobra.Command {
return &cobra.Command{
Use: "test",
Short: "Builds the specified targets and runs all test targets among them.",
Expand All @@ -39,12 +55,32 @@ specify targets.
RunE: interceptors.Run(
[]interceptors.Interceptor{
interceptors.WorkspaceRootInterceptor(),
pluginSystem.BESBackendInterceptor(),
},
func(ctx context.Context, cmd *cobra.Command, args []string) (exitErr error) {
isInteractiveMode, err := cmd.Root().PersistentFlags().GetBool(rootFlags.InteractiveFlagName)
if err != nil {
return err
}

defer func() {
errs := pluginSystem.ExecutePostTest(isInteractiveMode).Errors()
if len(errs) > 0 {
for _, err := range errs {
fmt.Fprintf(streams.Stderr, "Error: failed to run build command: %v\n", err)
}
var err *aspecterrors.ExitError
if errors.As(exitErr, &err) {
err.ExitCode = 1
}
}
}()

workspaceRoot := ctx.Value(interceptors.WorkspaceRootKey).(string)
bzl.SetWorkspaceRoot(workspaceRoot)
t := test.New(streams, bzl)
return t.Run(cmd, args)
besBackend := ctx.Value(system.BESBackendInterceptorKey).(bep.BESBackend)
return t.Run(args, besBackend)
},
),
}
Expand Down
2 changes: 2 additions & 0 deletions pkg/aspect/test/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ go_library(
"//pkg/aspecterrors",
"//pkg/bazel",
"//pkg/ioutils",
"//pkg/plugin/system/bep",
"@com_github_spf13_cobra//:cobra",
],
)
Expand All @@ -20,6 +21,7 @@ go_test(
":test",
"//pkg/bazel/mock",
"//pkg/ioutils",
"//pkg/plugin/system/bep/mock",
"@com_github_golang_mock//gomock",
"@com_github_onsi_gomega//:gomega",
],
Expand Down
31 changes: 22 additions & 9 deletions pkg/aspect/test/test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,12 @@ Not licensed for re-use.
package test

import (
"aspect.build/cli/pkg/bazel"
"aspect.build/cli/pkg/ioutils"
"github.com/spf13/cobra"
"fmt"

"aspect.build/cli/pkg/aspecterrors"
"aspect.build/cli/pkg/bazel"
"aspect.build/cli/pkg/ioutils"
"aspect.build/cli/pkg/plugin/system/bep"
)

type Test struct {
Expand All @@ -26,14 +27,26 @@ func New(streams ioutils.Streams, bzl bazel.Bazel) *Test {
}
}

func (t *Test) Run(_ *cobra.Command, args []string) error {
bazelCmd := []string{"test"}
func (t *Test) Run(args []string, besBackend bep.BESBackend) (exitErr error) {
besBackendFlag := fmt.Sprintf("--bes_backend=grpc://%s", besBackend.Addr())
bazelCmd := []string{"test", besBackendFlag}
bazelCmd = append(bazelCmd, args...)

if exitCode, err := t.bzl.Spawn(bazelCmd); exitCode != 0 {
err = &aspecterrors.ExitError{
Err: err,
ExitCode: exitCode,
exitCode, bazelErr := t.bzl.Spawn(bazelCmd)

// Process the subscribers errors before the Bazel one.
subscriberErrors := besBackend.Errors()
if len(subscriberErrors) > 0 {
for _, err := range subscriberErrors {
fmt.Fprintf(t.Streams.Stderr, "Error: failed to run test command: %v\n", err)
}
exitCode = 1
}

if exitCode != 0 {
err := &aspecterrors.ExitError{ExitCode: exitCode}
if bazelErr != nil {
err.Err = bazelErr
}
return err
}
Expand Down
21 changes: 17 additions & 4 deletions pkg/aspect/test/test_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,13 @@ package test_test
import (
"testing"

"github.com/golang/mock/gomock"
. "github.com/onsi/gomega"

"aspect.build/cli/pkg/aspect/test"
"aspect.build/cli/pkg/bazel/mock"
"aspect.build/cli/pkg/ioutils"
"github.com/golang/mock/gomock"
. "github.com/onsi/gomega"
bep_mock "aspect.build/cli/pkg/plugin/system/bep/mock"
JesseTatasciore marked this conversation as resolved.
Show resolved Hide resolved
)

// Embrace the stutter :)
Expand All @@ -20,10 +22,21 @@ func TestTest(t *testing.T) {
bzl := mock.NewMockBazel(ctrl)
bzl.
EXPECT().
Spawn([]string{"test"}).
Spawn([]string{"test", "--bes_backend=grpc://127.0.0.1:12345"}).
Return(0, nil)

besBackend := bep_mock.NewMockBESBackend(ctrl)
besBackend.
EXPECT().
Addr().
Return("127.0.0.1:12345").
Times(1)
besBackend.
EXPECT().
Errors().
Times(1)

b := test.New(ioutils.Streams{}, bzl)
g.Expect(b.Run(nil, []string{})).Should(Succeed())
g.Expect(b.Run([]string{}, besBackend)).Should(Succeed())
})
}
40 changes: 40 additions & 0 deletions pkg/plugin/sdk/v1alpha2/plugin/grpc.go
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,25 @@ func (m *GRPCServer) PostBuildHook(
m.Impl.PostBuildHook(req.IsInteractiveMode, prompter)
}

// PostTestHook translates the gRPC call to the Plugin PostTestHook
// implementation. It starts a prompt runner that is passed to the Plugin
// instance to be able to perform prompt actions to the CLI user.
func (m *GRPCServer) PostTestHook(
ctx context.Context,
req *proto.PostTestHookReq,
) (*proto.PostTestHookRes, error) {
conn, err := m.broker.Dial(req.BrokerId)
if err != nil {
return nil, err
}
defer conn.Close()

client := proto.NewPrompterClient(conn)
prompter := &PrompterGRPCClient{client: client}
return &proto.PostTestHookRes{},
m.Impl.PostTestHook(req.IsInteractiveMode, prompter)
}

// GRPCClient implements the gRPC client that is used by the Core to communicate
// with the Plugin instances.
type GRPCClient struct {
Expand Down Expand Up @@ -109,6 +128,27 @@ func (m *GRPCClient) PostBuildHook(isInteractiveMode bool, promptRunner ioutils.
return err
}

// PostTestHook is called from the Core to execute the Plugin PostTestHook. It
// starts the prompt runner server with the provided PromptRunner.
func (m *GRPCClient) PostTestHook(isInteractiveMode bool, promptRunner ioutils.PromptRunner) error {
prompterServer := &PrompterGRPCServer{promptRunner: promptRunner}
var s *grpc.Server
serverFunc := func(opts []grpc.ServerOption) *grpc.Server {
s = grpc.NewServer(opts...)
proto.RegisterPrompterServer(s, prompterServer)
return s
}
brokerID := m.broker.NextId()
go m.broker.AcceptAndServe(brokerID, serverFunc)
req := &proto.PostTestHookReq{
BrokerId: brokerID,
IsInteractiveMode: isInteractiveMode,
}
_, err := m.client.PostTestHook(context.Background(), req)
s.Stop()
return err
}

// PrompterGRPCServer implements the gRPC server that runs on the Core and is
// passed to the Plugin to allow prompt actions to the CLI user.
type PrompterGRPCServer struct {
Expand Down
4 changes: 4 additions & 0 deletions pkg/plugin/sdk/v1alpha2/plugin/interface.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,8 @@ type Plugin interface {
isInteractiveMode bool,
promptRunner ioutils.PromptRunner,
) error
PostTestHook(
isInteractiveMode bool,
promptRunner ioutils.PromptRunner,
) error
}
8 changes: 8 additions & 0 deletions pkg/plugin/sdk/v1alpha2/proto/plugin.proto
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ option go_package = "aspect.build/cli/pkg/plugin/sdk/v1alpha2/proto";
service Plugin {
rpc BEPEventCallback(BEPEventCallbackReq) returns (BEPEventCallbackRes);
rpc PostBuildHook(PostBuildHookReq) returns (PostBuildHookRes);
rpc PostTestHook(PostTestHookReq) returns (PostTestHookRes);
}

message BEPEventCallbackReq {
Expand All @@ -25,6 +26,13 @@ message PostBuildHookReq {

message PostBuildHookRes {}

message PostTestHookReq {
uint32 broker_id = 1;
bool is_interactive_mode = 2;
}

message PostTestHookRes {}

// Prompter is the service used by the Plugin instances to request prompt
// actions to the Core from the CLI users.
service Prompter {
Expand Down
4 changes: 2 additions & 2 deletions pkg/plugin/system/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@ go_library(
"//pkg/aspecterrors",
"//pkg/interceptors",
"//pkg/ioutils",
"//pkg/plugin/sdk/v1alpha1/config",
"//pkg/plugin/sdk/v1alpha1/plugin",
"//pkg/plugin/sdk/v1alpha2/config",
"//pkg/plugin/sdk/v1alpha2/plugin",
"//pkg/plugin/system/bep",
"@com_github_hashicorp_go_hclog//:go-hclog",
"@com_github_hashicorp_go_plugin//:go-plugin",
Expand Down
2 changes: 1 addition & 1 deletion pkg/plugin/system/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,4 +38,4 @@ on which hooks are exposed.

## Current SDK

See [the current SDK README](/pkg/plugin/sdk/v1alpha1/README.md).
See [the current SDK README](/pkg/plugin/sdk/v1alpha2/README.md).
16 changes: 14 additions & 2 deletions pkg/plugin/system/system.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,8 @@ import (
"aspect.build/cli/pkg/aspecterrors"
"aspect.build/cli/pkg/interceptors"
"aspect.build/cli/pkg/ioutils"
"aspect.build/cli/pkg/plugin/sdk/v1alpha1/config"
"aspect.build/cli/pkg/plugin/sdk/v1alpha1/plugin"
"aspect.build/cli/pkg/plugin/sdk/v1alpha2/config"
"aspect.build/cli/pkg/plugin/sdk/v1alpha2/plugin"
"aspect.build/cli/pkg/plugin/system/bep"
)

Expand All @@ -31,6 +31,7 @@ type PluginSystem interface {
TearDown()
BESBackendInterceptor() interceptors.Interceptor
ExecutePostBuild(isInteractiveMode bool) *aspecterrors.ErrorList
ExecutePostTest(isInteractiveMode bool) *aspecterrors.ErrorList
}

type pluginSystem struct {
Expand Down Expand Up @@ -155,6 +156,17 @@ func (ps *pluginSystem) ExecutePostBuild(isInteractiveMode bool) *aspecterrors.E
return errors
}

// ExecutePostTest executes all post-build hooks from all plugins.
func (ps *pluginSystem) ExecutePostTest(isInteractiveMode bool) *aspecterrors.ErrorList {
errors := &aspecterrors.ErrorList{}
for node := ps.plugins.head; node != nil; node = node.next {
if err := node.plugin.PostTestHook(isInteractiveMode, ps.promptRunner); err != nil {
errors.Insert(err)
}
}
return errors
}

// ClientFactory hides the call to goplugin.NewClient.
type ClientFactory interface {
New(*goplugin.ClientConfig) ClientProvider
Expand Down
2 changes: 1 addition & 1 deletion plugins/fix-visibility/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ go_library(
deps = [
"//bazel/buildeventstream/proto",
"//pkg/ioutils",
"//pkg/plugin/sdk/v1alpha1/config",
"//pkg/plugin/sdk/v1alpha2/config",
"@bazel_gazelle//label:go_default_library",
"@com_github_bazelbuild_buildtools//edit:go_default_library",
"@com_github_hashicorp_go_plugin//:go-plugin",
Expand Down
13 changes: 12 additions & 1 deletion plugins/fix-visibility/plugin.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ import (

buildeventstream "aspect.build/cli/bazel/buildeventstream/proto"
"aspect.build/cli/pkg/ioutils"
"aspect.build/cli/pkg/plugin/sdk/v1alpha1/config"
"aspect.build/cli/pkg/plugin/sdk/v1alpha2/config"
)

func main() {
Expand Down Expand Up @@ -154,6 +154,17 @@ func (plugin *FixVisibilityPlugin) PostBuildHook(
return nil
}

// PostTestHook satisfies the Plugin interface. It prompts the user for
// automatic fixes when in interactive mode. If the user rejects the automatic
// fixes, or if running in non-interactive mode, the commands to perform the fixes
// are printed to the terminal.
func (plugin *FixVisibilityPlugin) PostTestHook(
isInteractiveMode bool,
promptRunner ioutils.PromptRunner,
) error {
return plugin.PostBuildHook(isInteractiveMode, promptRunner)
}

func (plugin *FixVisibilityPlugin) hasPrivateVisibility(toFix string) (bool, error) {
visibility, err := plugin.buildozer.run("print visibility", toFix)
if err != nil {
Expand Down